@arronqzy/react-view 1.0.22 → 1.0.24
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/package.json +11 -11
- package/src/panel/ReactViewPanel.tsx +109 -965
- package/src/panel/components/PanelConfigSidebar.tsx +297 -3712
- package/src/panel/components/PanelHistoryPopover.tsx +163 -0
- package/src/panel/components/PanelLayerDock.tsx +412 -0
- package/src/panel/components/PanelMenubar.tsx +149 -0
- package/src/panel/components/PanelWorkspaceDialogs.tsx +271 -0
- package/src/panel/components/config/PanelConfigAudioSection.tsx +334 -0
- package/src/panel/components/config/PanelConfigChartSection.tsx +799 -0
- package/src/panel/components/config/PanelConfigGeometrySection.tsx +233 -0
- package/src/panel/components/config/PanelConfigGridChildSpan.tsx +70 -0
- package/src/panel/components/config/PanelConfigGridSection.tsx +118 -0
- package/src/panel/components/config/PanelConfigMultiSelect.tsx +1291 -0
- package/src/panel/components/config/PanelConfigNodeInfo.tsx +169 -0
- package/src/panel/components/config/PanelConfigReferenceSection.tsx +99 -0
- package/src/panel/components/config/PanelConfigScene3dSection.tsx +6 -2
- package/src/panel/components/config/PanelConfigStyleSections.tsx +249 -0
- package/src/panel/components/config/PanelConfigTextSection.tsx +268 -0
- package/src/panel/components/config/PanelConfigVideoSection.tsx +133 -0
- package/src/panel/components/config/PanelConfigViewportSection.tsx +139 -0
- package/src/panel/components/config/helpers.ts +85 -0
- package/src/panel/icons.tsx +160 -0
- package/src/panel/utils/panelSelection.ts +57 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect,
|
|
3
|
+
useMemo,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
} from "react";
|
|
2
7
|
import { useI18n } from "@arronqzy/i18n/react";
|
|
3
8
|
import {
|
|
4
|
-
Card,
|
|
5
|
-
CardContent,
|
|
6
|
-
CardHeader,
|
|
7
|
-
CardTitle,
|
|
8
9
|
Checkbox,
|
|
9
10
|
Collapsible,
|
|
10
11
|
CollapsibleContent,
|
|
@@ -14,25 +15,9 @@ import {
|
|
|
14
15
|
EmptyIcon,
|
|
15
16
|
EmptyTitle,
|
|
16
17
|
Input,
|
|
17
|
-
Select,
|
|
18
|
-
SelectContent,
|
|
19
|
-
SelectItem,
|
|
20
|
-
SelectTrigger,
|
|
21
|
-
SelectValue,
|
|
22
|
-
Textarea,
|
|
23
18
|
} from "@arronqzy/ui";
|
|
24
|
-
import type {
|
|
25
|
-
|
|
26
|
-
PanelElement,
|
|
27
|
-
PanelElementStyle,
|
|
28
|
-
ReferenceCopyMode,
|
|
29
|
-
} from "../types";
|
|
30
|
-
import { buildChartOption, CHART_TYPES, getChartLabelsDisplayText, getChartValuesDisplayText } from "../utils/chartOptionBuilder";
|
|
31
|
-
import {
|
|
32
|
-
VIEWPORT_OVERFLOW_MODES,
|
|
33
|
-
normalizeViewportOverflow,
|
|
34
|
-
type ViewportOverflowMode,
|
|
35
|
-
} from "../utils/viewportPlacement";
|
|
19
|
+
import type { PanelElement, ReferenceCopyMode } from "../types";
|
|
20
|
+
import { CHART_TYPES } from "../utils/chartOptionBuilder";
|
|
36
21
|
import type { PanelLayer } from "../types";
|
|
37
22
|
import { getPanelMessages } from "../constants/messages";
|
|
38
23
|
import { ViewElementScopePanel } from "./ViewElementScopePanel";
|
|
@@ -42,16 +27,27 @@ import { collectElementScopeWarnings } from "../utils/scope-template-warnings";
|
|
|
42
27
|
import { ScopeConfigProvider } from "./scope-config/ScopeConfigContext";
|
|
43
28
|
import { ScopeTemplateWarningsPanel } from "./scope-config/ScopeTemplateWarningsPanel";
|
|
44
29
|
import { PanelConfigTableSection } from "./table/PanelConfigTableSection";
|
|
45
|
-
import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
|
|
46
30
|
import { highlightConfigField, subscribeRevealPanelConfig } from "../ai/revealConfigField";
|
|
47
31
|
import { PanelConfigScene3dSection } from "./config/PanelConfigScene3dSection";
|
|
48
|
-
import {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
32
|
+
import { PanelConfigNodeInfo } from "./config/PanelConfigNodeInfo";
|
|
33
|
+
import { PanelConfigChartSection } from "./config/PanelConfigChartSection";
|
|
34
|
+
import { PanelConfigTextSection } from "./config/PanelConfigTextSection";
|
|
35
|
+
import { PanelConfigStyleSections } from "./config/PanelConfigStyleSections";
|
|
36
|
+
import { PanelConfigGridSection } from "./config/PanelConfigGridSection";
|
|
37
|
+
import { PanelConfigGridChildSpan } from "./config/PanelConfigGridChildSpan";
|
|
38
|
+
import { PanelConfigViewportSection } from "./config/PanelConfigViewportSection";
|
|
39
|
+
import { PanelConfigReferenceSection } from "./config/PanelConfigReferenceSection";
|
|
40
|
+
import { PanelConfigAudioSection } from "./config/PanelConfigAudioSection";
|
|
41
|
+
import { PanelConfigVideoSection } from "./config/PanelConfigVideoSection";
|
|
42
|
+
import { PanelConfigGeometrySection } from "./config/PanelConfigGeometrySection";
|
|
43
|
+
import { PanelConfigMultiSelect } from "./config/PanelConfigMultiSelect";
|
|
44
|
+
import {
|
|
45
|
+
type ConfigSectionHelpers,
|
|
46
|
+
type UpdateElement,
|
|
47
|
+
OPTION_CHECKBOX_CLASS,
|
|
48
|
+
OPTION_INPUT_CLASS,
|
|
49
|
+
OPTION_SELECT_TRIGGER_CLASS,
|
|
50
|
+
} from "./config/helpers";
|
|
55
51
|
|
|
56
52
|
export type PanelConfigSidebarProps = {
|
|
57
53
|
selectedElement: PanelElement | null;
|
|
@@ -65,11 +61,29 @@ export type PanelConfigSidebarProps = {
|
|
|
65
61
|
nodeId: string,
|
|
66
62
|
action: "bringForward" | "sendBackward" | "bringToFront" | "sendToBack"
|
|
67
63
|
) => void;
|
|
68
|
-
/** 蓝图视图绑定节点写入的信号 scope,用于展示与模版解析 */
|
|
69
64
|
viewElementScope?: unknown;
|
|
70
65
|
blueprintNodeOptions?: { id: string; label: string }[];
|
|
71
66
|
};
|
|
72
67
|
|
|
68
|
+
function ConfigSearchEmpty({
|
|
69
|
+
hasSearch,
|
|
70
|
+
matchCountRef,
|
|
71
|
+
label,
|
|
72
|
+
}: {
|
|
73
|
+
hasSearch: boolean;
|
|
74
|
+
matchCountRef: React.MutableRefObject<number>;
|
|
75
|
+
label: string;
|
|
76
|
+
}) {
|
|
77
|
+
if (hasSearch && matchCountRef.current === 0) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
80
|
+
{label}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
export function PanelConfigSidebar({
|
|
74
88
|
selectedElement,
|
|
75
89
|
selectedElements = [],
|
|
@@ -85,26 +99,11 @@ export function PanelConfigSidebar({
|
|
|
85
99
|
const { t } = useI18n();
|
|
86
100
|
const messages = React.useMemo(() => getPanelMessages(t), [t]);
|
|
87
101
|
|
|
88
|
-
const [isAdvancedOptionMode, setIsAdvancedOptionMode] = useState(false);
|
|
89
|
-
const [optionJsonText, setOptionJsonText] = useState("{}");
|
|
90
|
-
const [optionJsonError, setOptionJsonError] = useState<string | null>(null);
|
|
91
|
-
const [uploadStatus, setUploadStatus] = useState<string>("");
|
|
92
|
-
const [audioStatus, setAudioStatus] = useState<string>("");
|
|
93
|
-
const [videoStatus, setVideoStatus] = useState<string>("");
|
|
94
|
-
const [geometryDrawPenColor, setGeometryDrawPenColor] = useState("#111827");
|
|
95
|
-
const [geometryDrawPenWidth, setGeometryDrawPenWidth] = useState(3);
|
|
96
|
-
const [isGeometryDrawing, setIsGeometryDrawing] = useState(false);
|
|
97
102
|
const [configSearch, setConfigSearch] = useState("");
|
|
98
103
|
const [isSearchCollapsed, setIsSearchCollapsed] = useState(false);
|
|
99
104
|
const SEARCH_COLLAPSE_STORAGE_KEY = "panel:config-search-collapsed";
|
|
100
|
-
const textEditorRef = useRef<HTMLDivElement | null>(null);
|
|
101
|
-
const geometryDrawCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
102
|
-
const geometryLastPointRef = useRef<{ x: number; y: number } | null>(null);
|
|
103
|
-
const recorderRef = useRef<MediaRecorder | null>(null);
|
|
104
|
-
const recordStreamRef = useRef<MediaStream | null>(null);
|
|
105
|
-
const audioChunksRef = useRef<BlobPart[]>([]);
|
|
106
|
-
const [isRecordingAudio, setIsRecordingAudio] = useState(false);
|
|
107
105
|
const sidebarScrollRef = useRef<HTMLElement | null>(null);
|
|
106
|
+
const matchCountRef = useRef(0);
|
|
108
107
|
const themedScrollbarClass =
|
|
109
108
|
"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";
|
|
110
109
|
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
|
|
@@ -118,7 +117,6 @@ export function PanelConfigSidebar({
|
|
|
118
117
|
chartAdvancedAxisPointer: false,
|
|
119
118
|
reference: true,
|
|
120
119
|
});
|
|
121
|
-
const [expandedNodeCards, setExpandedNodeCards] = useState<Record<string, boolean>>({});
|
|
122
120
|
|
|
123
121
|
const isChartElement = !!selectedElement && CHART_TYPES.has(selectedElement.materialType ?? "");
|
|
124
122
|
const selectedLayer = selectedElement
|
|
@@ -133,16 +131,6 @@ export function PanelConfigSidebar({
|
|
|
133
131
|
: selectedLayer?.locked
|
|
134
132
|
? messages.nodeConfigLayerLocked
|
|
135
133
|
: "";
|
|
136
|
-
const selectedChartType = (selectedElement?.materialType ?? "") as
|
|
137
|
-
| "bar"
|
|
138
|
-
| "line"
|
|
139
|
-
| "pie"
|
|
140
|
-
| "area"
|
|
141
|
-
| "scatter"
|
|
142
|
-
| "radar"
|
|
143
|
-
| "gauge"
|
|
144
|
-
| "funnel"
|
|
145
|
-
| "";
|
|
146
134
|
const normalizedSearch = useMemo(() => configSearch.trim().toLowerCase(), [configSearch]);
|
|
147
135
|
const hasSearch = normalizedSearch.length > 0;
|
|
148
136
|
const showScopePanel =
|
|
@@ -152,27 +140,13 @@ export function PanelConfigSidebar({
|
|
|
152
140
|
const scopeWarnings = useMemo(() => {
|
|
153
141
|
if (!selectedElement || viewElementScope === undefined) return [];
|
|
154
142
|
return collectElementScopeWarnings(selectedElement, viewElementScope, t);
|
|
155
|
-
}, [selectedElement, viewElementScope]);
|
|
143
|
+
}, [selectedElement, viewElementScope, t]);
|
|
156
144
|
const effectiveSelectedElements = useMemo(() => {
|
|
157
145
|
if (selectedElements.length > 0) return selectedElements;
|
|
158
146
|
return selectedElement ? [selectedElement] : [];
|
|
159
147
|
}, [selectedElement, selectedElements]);
|
|
160
148
|
const isMultiSelectMode = effectiveSelectedElements.length > 1;
|
|
161
149
|
|
|
162
|
-
useEffect(() => {
|
|
163
|
-
if (!selectedElement) {
|
|
164
|
-
setOptionJsonText("{}");
|
|
165
|
-
setOptionJsonError(null);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (CHART_TYPES.has(selectedElement.materialType ?? "")) {
|
|
169
|
-
setOptionJsonText(JSON.stringify(buildChartOption(selectedElement), null, 2));
|
|
170
|
-
} else {
|
|
171
|
-
setOptionJsonText("{}");
|
|
172
|
-
}
|
|
173
|
-
setOptionJsonError(null);
|
|
174
|
-
}, [selectedElement]);
|
|
175
|
-
|
|
176
150
|
useEffect(() => {
|
|
177
151
|
if (!selectedElement) return;
|
|
178
152
|
if (!CHART_TYPES.has(selectedElement.materialType ?? "")) return;
|
|
@@ -182,14 +156,6 @@ export function PanelConfigSidebar({
|
|
|
182
156
|
}));
|
|
183
157
|
}, [selectedElement?.id, selectedElement?.materialType]);
|
|
184
158
|
|
|
185
|
-
useEffect(() => {
|
|
186
|
-
if (!selectedElement || selectedElement.materialType !== "text") return;
|
|
187
|
-
const nextHtml =
|
|
188
|
-
selectedElement.textHtml ?? `<p>${t("panel.defaults.doubleClickTextHtml")}</p>`;
|
|
189
|
-
if (textEditorRef.current && textEditorRef.current.innerHTML !== nextHtml) {
|
|
190
|
-
textEditorRef.current.innerHTML = nextHtml;
|
|
191
|
-
}
|
|
192
|
-
}, [selectedElement?.id, selectedElement?.materialType, selectedElement?.textHtml]);
|
|
193
159
|
useEffect(() => {
|
|
194
160
|
if (typeof window === "undefined") return;
|
|
195
161
|
const saved = window.localStorage.getItem(SEARCH_COLLAPSE_STORAGE_KEY);
|
|
@@ -199,234 +165,6 @@ export function PanelConfigSidebar({
|
|
|
199
165
|
if (typeof window === "undefined") return;
|
|
200
166
|
window.localStorage.setItem(SEARCH_COLLAPSE_STORAGE_KEY, isSearchCollapsed ? "1" : "0");
|
|
201
167
|
}, [isSearchCollapsed]);
|
|
202
|
-
useEffect(
|
|
203
|
-
() => () => {
|
|
204
|
-
recordStreamRef.current?.getTracks().forEach((track) => track.stop());
|
|
205
|
-
},
|
|
206
|
-
[]
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
const updateSelectedChart = useCallback(
|
|
210
|
-
(patch: Partial<PanelChartConfig>) => {
|
|
211
|
-
if (!selectedElement) return;
|
|
212
|
-
updateElement(selectedElement.id, {
|
|
213
|
-
chart: { ...(selectedElement.chart ?? {}), ...patch },
|
|
214
|
-
});
|
|
215
|
-
},
|
|
216
|
-
[selectedElement, updateElement]
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
const updateSelectedStyle = useCallback(
|
|
220
|
-
(patch: Partial<PanelElementStyle>) => {
|
|
221
|
-
if (!selectedElement) return;
|
|
222
|
-
updateElement(selectedElement.id, {
|
|
223
|
-
style: { ...(selectedElement.style ?? {}), ...patch },
|
|
224
|
-
});
|
|
225
|
-
},
|
|
226
|
-
[selectedElement, updateElement]
|
|
227
|
-
);
|
|
228
|
-
const updateSelectedText = useCallback(
|
|
229
|
-
(patch: Partial<PanelElement>) => {
|
|
230
|
-
if (!selectedElement || selectedElement.materialType !== "text") return;
|
|
231
|
-
updateElement(selectedElement.id, patch);
|
|
232
|
-
},
|
|
233
|
-
[selectedElement, updateElement]
|
|
234
|
-
);
|
|
235
|
-
const updateSelectedGeometry = useCallback(
|
|
236
|
-
(patch: Partial<PanelElement>) => {
|
|
237
|
-
if (!selectedElement || selectedElement.materialType !== "geometry") return;
|
|
238
|
-
updateElement(selectedElement.id, patch);
|
|
239
|
-
},
|
|
240
|
-
[selectedElement, updateElement]
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
const redrawGeometryPadFromElement = useCallback(() => {
|
|
244
|
-
const canvas = geometryDrawCanvasRef.current;
|
|
245
|
-
if (!canvas) return;
|
|
246
|
-
const ctx = canvas.getContext("2d");
|
|
247
|
-
if (!ctx) return;
|
|
248
|
-
const width = canvas.width;
|
|
249
|
-
const height = canvas.height;
|
|
250
|
-
ctx.clearRect(0, 0, width, height);
|
|
251
|
-
ctx.fillStyle = "#ffffff";
|
|
252
|
-
ctx.fillRect(0, 0, width, height);
|
|
253
|
-
const sketch = selectedElement?.geometrySketchDataUrl;
|
|
254
|
-
if (!sketch) return;
|
|
255
|
-
const img = new Image();
|
|
256
|
-
img.onload = () => {
|
|
257
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
258
|
-
};
|
|
259
|
-
img.src = sketch;
|
|
260
|
-
}, [selectedElement?.geometrySketchDataUrl]);
|
|
261
|
-
|
|
262
|
-
useEffect(() => {
|
|
263
|
-
if (!selectedElement || selectedElement.materialType !== "geometry") return;
|
|
264
|
-
redrawGeometryPadFromElement();
|
|
265
|
-
}, [redrawGeometryPadFromElement, selectedElement?.id, selectedElement?.materialType]);
|
|
266
|
-
|
|
267
|
-
const handleUploadBackgroundImage = useCallback(
|
|
268
|
-
async (file: File) => {
|
|
269
|
-
if (!selectedElement) return;
|
|
270
|
-
try {
|
|
271
|
-
await runBusyTask(t("common.uploadingFile"), async () => {
|
|
272
|
-
const base64 = await readFileAsDataUrl(file, messages.readImageFailed, "image");
|
|
273
|
-
updateSelectedStyle({
|
|
274
|
-
backgroundImage: `url("${base64}")`,
|
|
275
|
-
});
|
|
276
|
-
setUploadStatus(t("panel.config.uploadWrittenBase64"));
|
|
277
|
-
try {
|
|
278
|
-
const form = new FormData();
|
|
279
|
-
form.append("file", file);
|
|
280
|
-
const resp = await fetch("/api/upload", { method: "POST", body: form });
|
|
281
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
282
|
-
const data = (await resp.json()) as { url?: string };
|
|
283
|
-
if (data.url) {
|
|
284
|
-
updateSelectedStyle({ backgroundImageRemoteUrl: data.url });
|
|
285
|
-
setUploadStatus(t("panel.config.uploadServerAndBase64"));
|
|
286
|
-
}
|
|
287
|
-
} catch {
|
|
288
|
-
setUploadStatus(t("panel.config.uploadServerFailedKeepBase64"));
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
} catch (error) {
|
|
292
|
-
setUploadStatus(
|
|
293
|
-
error instanceof Error ? error.message : messages.readImageFailed
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
},
|
|
297
|
-
[messages.readImageFailed, selectedElement, t, updateSelectedStyle]
|
|
298
|
-
);
|
|
299
|
-
const updateSelectedAudio = useCallback(
|
|
300
|
-
(patch: Partial<PanelElement>) => {
|
|
301
|
-
if (!selectedElement || selectedElement.materialType !== "audio") return;
|
|
302
|
-
updateElement(selectedElement.id, patch);
|
|
303
|
-
},
|
|
304
|
-
[selectedElement, updateElement]
|
|
305
|
-
);
|
|
306
|
-
const handleUploadAudioFile = useCallback(
|
|
307
|
-
async (file: File) => {
|
|
308
|
-
if (!selectedElement || selectedElement.materialType !== "audio") return;
|
|
309
|
-
try {
|
|
310
|
-
await runBusyTask(t("common.uploadingFile"), async () => {
|
|
311
|
-
const base64 = await readFileAsDataUrl(file, messages.readAudioFailed, "audio");
|
|
312
|
-
updateSelectedAudio({ audioSrc: base64 });
|
|
313
|
-
setAudioStatus(messages.audioLocalSaved);
|
|
314
|
-
try {
|
|
315
|
-
const form = new FormData();
|
|
316
|
-
form.append("file", file);
|
|
317
|
-
const resp = await fetch("/api/upload", { method: "POST", body: form });
|
|
318
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
319
|
-
const data = (await resp.json()) as { url?: string };
|
|
320
|
-
if (data.url) {
|
|
321
|
-
updateSelectedAudio({ audioRemoteUrl: data.url });
|
|
322
|
-
setAudioStatus(messages.audioRemoteUploaded);
|
|
323
|
-
}
|
|
324
|
-
} catch {
|
|
325
|
-
setAudioStatus(messages.audioServerUploadFailed);
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
} catch (error) {
|
|
329
|
-
setAudioStatus(
|
|
330
|
-
error instanceof Error ? error.message : messages.readAudioFailed
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
[messages, selectedElement, t, updateSelectedAudio]
|
|
335
|
-
);
|
|
336
|
-
const handleUploadAudioPoster = useCallback(
|
|
337
|
-
async (file: File) => {
|
|
338
|
-
if (!selectedElement || selectedElement.materialType !== "audio") return;
|
|
339
|
-
try {
|
|
340
|
-
const base64 = await readFileAsDataUrl(file, messages.readImageFailed, "image");
|
|
341
|
-
updateSelectedAudio({ audioPosterImage: base64 });
|
|
342
|
-
setAudioStatus(messages.audioPosterSet);
|
|
343
|
-
} catch (error) {
|
|
344
|
-
setAudioStatus(
|
|
345
|
-
error instanceof Error ? error.message : messages.readImageFailed
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
},
|
|
349
|
-
[messages, selectedElement, updateSelectedAudio]
|
|
350
|
-
);
|
|
351
|
-
const stopRecordingAudio = useCallback(() => {
|
|
352
|
-
recorderRef.current?.stop();
|
|
353
|
-
}, []);
|
|
354
|
-
const startRecordingAudio = useCallback(async () => {
|
|
355
|
-
if (!selectedElement || selectedElement.materialType !== "audio") return;
|
|
356
|
-
if (!navigator.mediaDevices?.getUserMedia) {
|
|
357
|
-
setAudioStatus(messages.audioRecordUnsupported);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
try {
|
|
361
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
362
|
-
recordStreamRef.current = stream;
|
|
363
|
-
const recorder = new MediaRecorder(stream);
|
|
364
|
-
recorderRef.current = recorder;
|
|
365
|
-
audioChunksRef.current = [];
|
|
366
|
-
recorder.ondataavailable = (event) => {
|
|
367
|
-
if (event.data.size > 0) audioChunksRef.current.push(event.data);
|
|
368
|
-
};
|
|
369
|
-
recorder.onstop = async () => {
|
|
370
|
-
const blob = new Blob(audioChunksRef.current, { type: recorder.mimeType || "audio/webm" });
|
|
371
|
-
try {
|
|
372
|
-
const dataUrl = await readFileAsDataUrl(blob, messages.readRecordAudioFailed, "audio");
|
|
373
|
-
updateSelectedAudio({ audioSrc: dataUrl });
|
|
374
|
-
setAudioStatus(messages.audioRecordSaved);
|
|
375
|
-
} catch (error) {
|
|
376
|
-
setAudioStatus(
|
|
377
|
-
error instanceof Error ? error.message : messages.readRecordAudioFailed
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
recordStreamRef.current?.getTracks().forEach((track) => track.stop());
|
|
381
|
-
recordStreamRef.current = null;
|
|
382
|
-
recorderRef.current = null;
|
|
383
|
-
setIsRecordingAudio(false);
|
|
384
|
-
};
|
|
385
|
-
recorder.start();
|
|
386
|
-
setIsRecordingAudio(true);
|
|
387
|
-
setAudioStatus(messages.audioRecording);
|
|
388
|
-
} catch {
|
|
389
|
-
setAudioStatus(messages.audioRecordStartFailed);
|
|
390
|
-
setIsRecordingAudio(false);
|
|
391
|
-
}
|
|
392
|
-
}, [selectedElement, updateSelectedAudio]);
|
|
393
|
-
const updateSelectedVideo = useCallback(
|
|
394
|
-
(patch: Partial<PanelElement>) => {
|
|
395
|
-
if (!selectedElement || selectedElement.materialType !== "video") return;
|
|
396
|
-
updateElement(selectedElement.id, patch);
|
|
397
|
-
},
|
|
398
|
-
[selectedElement, updateElement]
|
|
399
|
-
);
|
|
400
|
-
const handleUploadVideoFile = useCallback(
|
|
401
|
-
async (file: File) => {
|
|
402
|
-
if (!selectedElement || selectedElement.materialType !== "video") return;
|
|
403
|
-
try {
|
|
404
|
-
await runBusyTask(t("common.uploadingFile"), async () => {
|
|
405
|
-
const base64 = await readFileAsDataUrl(file, messages.readVideoFailed, "video");
|
|
406
|
-
updateSelectedVideo({ videoSrc: base64 });
|
|
407
|
-
setVideoStatus(messages.videoLocalSaved);
|
|
408
|
-
try {
|
|
409
|
-
const form = new FormData();
|
|
410
|
-
form.append("file", file);
|
|
411
|
-
const resp = await fetch("/api/upload", { method: "POST", body: form });
|
|
412
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
413
|
-
const data = (await resp.json()) as { url?: string };
|
|
414
|
-
if (data.url) {
|
|
415
|
-
updateSelectedVideo({ videoRemoteUrl: data.url });
|
|
416
|
-
setVideoStatus(messages.videoRemoteUploaded);
|
|
417
|
-
}
|
|
418
|
-
} catch {
|
|
419
|
-
setVideoStatus(messages.videoServerUploadFailed);
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
} catch (error) {
|
|
423
|
-
setVideoStatus(
|
|
424
|
-
error instanceof Error ? error.message : messages.readVideoFailed
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
},
|
|
428
|
-
[messages, selectedElement, t, updateSelectedVideo]
|
|
429
|
-
);
|
|
430
168
|
|
|
431
169
|
const isSectionExpanded = (key: string, defaultValue = true) =>
|
|
432
170
|
expandedSections[key] ?? defaultValue;
|
|
@@ -459,6 +197,8 @@ export function PanelConfigSidebar({
|
|
|
459
197
|
});
|
|
460
198
|
}, []);
|
|
461
199
|
|
|
200
|
+
matchCountRef.current = 0;
|
|
201
|
+
|
|
462
202
|
const renderSection = (
|
|
463
203
|
key: string,
|
|
464
204
|
title: string,
|
|
@@ -471,32 +211,32 @@ export function PanelConfigSidebar({
|
|
|
471
211
|
!hasSearch ||
|
|
472
212
|
[title, ...searchTerms].some((term) => term.toLowerCase().includes(normalizedSearch));
|
|
473
213
|
if (!hit) return null;
|
|
474
|
-
|
|
214
|
+
matchCountRef.current += 1;
|
|
475
215
|
return (
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
216
|
+
<Collapsible
|
|
217
|
+
open={hasSearch ? true : isSectionExpanded(key, defaultOpen)}
|
|
218
|
+
onOpenChange={(open) => setSectionExpanded(key, open)}
|
|
219
|
+
className="rounded-xl border border-border/70 bg-card/95 shadow-sm"
|
|
220
|
+
data-config-section={key}
|
|
221
|
+
>
|
|
222
|
+
<div className="flex items-center gap-1.5 px-3 py-2">
|
|
223
|
+
<CollapsibleTrigger asChild>
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
className="flex h-6 w-6 items-center justify-center rounded-md text-xs hover:bg-accent"
|
|
227
|
+
>
|
|
228
|
+
{isSectionExpanded(key, defaultOpen) ? "▾" : "▸"}
|
|
229
|
+
</button>
|
|
230
|
+
</CollapsibleTrigger>
|
|
231
|
+
<div className="flex min-w-0 flex-1 items-center gap-1">
|
|
232
|
+
<div className="text-[11px] font-semibold tracking-wide text-muted-foreground">{title}</div>
|
|
233
|
+
{hint ? <ConfigHintIcon label={title}>{hint}</ConfigHintIcon> : null}
|
|
234
|
+
</div>
|
|
494
235
|
</div>
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
</
|
|
499
|
-
</Collapsible>
|
|
236
|
+
<CollapsibleContent className="space-y-3 border-t border-border/60 bg-muted/[0.1] px-3 pb-3 pt-2.5">
|
|
237
|
+
{children}
|
|
238
|
+
</CollapsibleContent>
|
|
239
|
+
</Collapsible>
|
|
500
240
|
);
|
|
501
241
|
};
|
|
502
242
|
|
|
@@ -545,13 +285,6 @@ export function PanelConfigSidebar({
|
|
|
545
285
|
</Collapsible>
|
|
546
286
|
);
|
|
547
287
|
};
|
|
548
|
-
const isNodeCardExpanded = useCallback(
|
|
549
|
-
(id: string) => expandedNodeCards[id] ?? true,
|
|
550
|
-
[expandedNodeCards]
|
|
551
|
-
);
|
|
552
|
-
const setNodeCardExpanded = useCallback((id: string, open: boolean) => {
|
|
553
|
-
setExpandedNodeCards((prev) => ({ ...prev, [id]: open }));
|
|
554
|
-
}, []);
|
|
555
288
|
|
|
556
289
|
const renderColorField = (
|
|
557
290
|
label: string,
|
|
@@ -577,12 +310,7 @@ export function PanelConfigSidebar({
|
|
|
577
310
|
</div>
|
|
578
311
|
</label>
|
|
579
312
|
);
|
|
580
|
-
|
|
581
|
-
"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";
|
|
582
|
-
const optionInputClass =
|
|
583
|
-
"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";
|
|
584
|
-
const optionSelectTriggerClass =
|
|
585
|
-
"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";
|
|
313
|
+
|
|
586
314
|
const optionLabelTextClass = "truncate whitespace-nowrap text-[11px] leading-none";
|
|
587
315
|
const renderOptionLabel = (label: string, keyPath: string, desc: string) => (
|
|
588
316
|
<div className="flex min-w-0 items-center gap-1">
|
|
@@ -607,56 +335,24 @@ export function PanelConfigSidebar({
|
|
|
607
335
|
</ConfigHintIcon>
|
|
608
336
|
</div>
|
|
609
337
|
);
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
!Array.isArray(prev)
|
|
625
|
-
) {
|
|
626
|
-
output[key] = mergeOptionPatch(
|
|
627
|
-
prev as Record<string, unknown>,
|
|
628
|
-
value as Record<string, unknown>
|
|
629
|
-
);
|
|
630
|
-
} else {
|
|
631
|
-
output[key] = value;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
return output;
|
|
635
|
-
},
|
|
636
|
-
[]
|
|
637
|
-
);
|
|
638
|
-
const updateSelectedOptionForm = useCallback(
|
|
639
|
-
(patch: Record<string, unknown>) => {
|
|
640
|
-
if (!selectedElement) return;
|
|
641
|
-
updateSelectedChart({
|
|
642
|
-
option: mergeOptionPatch(
|
|
643
|
-
(selectedElement.chart?.option as Record<string, unknown> | undefined) ?? {},
|
|
644
|
-
patch
|
|
645
|
-
),
|
|
646
|
-
});
|
|
647
|
-
},
|
|
648
|
-
[mergeOptionPatch, selectedElement, updateSelectedChart]
|
|
649
|
-
);
|
|
650
|
-
const execTextCommand = (cmd: "bold" | "italic" | "underline") => {
|
|
651
|
-
textEditorRef.current?.focus();
|
|
652
|
-
document.execCommand(cmd);
|
|
653
|
-
const next = textEditorRef.current?.innerHTML ?? "";
|
|
654
|
-
if (selectedElement?.materialType === "text") {
|
|
655
|
-
updateElement(selectedElement.id, { textHtml: next || "<p><br/></p>" });
|
|
656
|
-
}
|
|
338
|
+
|
|
339
|
+
const helpers: ConfigSectionHelpers = {
|
|
340
|
+
renderSection,
|
|
341
|
+
renderFieldGroup,
|
|
342
|
+
renderColorField,
|
|
343
|
+
hasSearch,
|
|
344
|
+
normalizedSearch,
|
|
345
|
+
isSectionExpanded,
|
|
346
|
+
setSectionExpanded,
|
|
347
|
+
optionCheckboxClass: OPTION_CHECKBOX_CLASS,
|
|
348
|
+
optionInputClass: OPTION_INPUT_CLASS,
|
|
349
|
+
optionSelectTriggerClass: OPTION_SELECT_TRIGGER_CLASS,
|
|
350
|
+
renderOptionLabel,
|
|
351
|
+
renderFormatterLabel,
|
|
657
352
|
};
|
|
658
353
|
|
|
659
|
-
|
|
354
|
+
const materialType = selectedElement?.materialType ?? "";
|
|
355
|
+
|
|
660
356
|
return (
|
|
661
357
|
<ScopeConfigProvider
|
|
662
358
|
scope={showScopePanel ? viewElementScope : undefined}
|
|
@@ -664,3340 +360,229 @@ export function PanelConfigSidebar({
|
|
|
664
360
|
warnings={scopeWarnings}
|
|
665
361
|
scrollContainerRef={sidebarScrollRef}
|
|
666
362
|
>
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
</div>
|
|
675
|
-
{showScopePanel ? (
|
|
676
|
-
<ViewElementScopePanel scope={viewElementScope} />
|
|
677
|
-
) : null}
|
|
678
|
-
<ScopeTemplateWarningsPanel />
|
|
679
|
-
<div
|
|
680
|
-
className={
|
|
681
|
-
showScopePanel || scopeWarnings.length > 0
|
|
682
|
-
? "mt-2 border-t border-border/50 pt-2"
|
|
683
|
-
: "mt-0"
|
|
684
|
-
}
|
|
685
|
-
>
|
|
686
|
-
<div className="flex items-center justify-end gap-2">
|
|
687
|
-
<button
|
|
688
|
-
type="button"
|
|
689
|
-
className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
|
|
690
|
-
onClick={() => setIsSearchCollapsed((prev) => !prev)}
|
|
691
|
-
>
|
|
692
|
-
{isSearchCollapsed ? t("panel.config.expandSearch") : t("panel.config.collapseSearch")}
|
|
693
|
-
</button>
|
|
363
|
+
<aside
|
|
364
|
+
ref={sidebarScrollRef}
|
|
365
|
+
className={`scope-config-sidebar h-full overflow-auto border-l border-border bg-muted/[0.14] px-3 py-3 text-foreground [&_.config-field--highlight]:rounded-md [&_.config-field--highlight]:ring-2 [&_.config-field--highlight]:ring-sky-400/80 [&_.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}`}
|
|
366
|
+
>
|
|
367
|
+
<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">
|
|
368
|
+
<div className="flex items-center justify-between gap-2">
|
|
369
|
+
<div className="text-xs font-semibold tracking-wide">{t("panel.config.panelTitle")}</div>
|
|
694
370
|
</div>
|
|
695
|
-
{
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
</div>
|
|
710
|
-
) : null}
|
|
711
|
-
</div>
|
|
712
|
-
</div>
|
|
713
|
-
{isMultiSelectMode ? (
|
|
714
|
-
<div className="space-y-3">
|
|
715
|
-
<Card>
|
|
716
|
-
<CardHeader className="pb-2">
|
|
717
|
-
<CardTitle className="text-xs">{t("panel.config.batchTitleWithCount", { count: effectiveSelectedElements.length })}</CardTitle>
|
|
718
|
-
</CardHeader>
|
|
719
|
-
<CardContent className="space-y-2 text-xs">
|
|
720
|
-
<div className="grid grid-cols-2 gap-2">
|
|
721
|
-
<button
|
|
722
|
-
type="button"
|
|
723
|
-
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
724
|
-
onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: true }))}
|
|
725
|
-
>
|
|
726
|
-
{t("panel.config.lockAll")}
|
|
727
|
-
</button>
|
|
728
|
-
<button
|
|
729
|
-
type="button"
|
|
730
|
-
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
731
|
-
onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: false }))}
|
|
732
|
-
>
|
|
733
|
-
{t("panel.config.unlockAll")}
|
|
734
|
-
</button>
|
|
735
|
-
<button
|
|
736
|
-
type="button"
|
|
737
|
-
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
738
|
-
onClick={() =>
|
|
739
|
-
effectiveSelectedElements.forEach((el) =>
|
|
740
|
-
onAdjustNodeZOrder?.(el.id, "bringForward")
|
|
741
|
-
)
|
|
742
|
-
}
|
|
743
|
-
>
|
|
744
|
-
{t("panel.config.bringAllForward")}
|
|
745
|
-
</button>
|
|
746
|
-
<button
|
|
747
|
-
type="button"
|
|
748
|
-
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
749
|
-
onClick={() =>
|
|
750
|
-
effectiveSelectedElements.forEach((el) =>
|
|
751
|
-
onAdjustNodeZOrder?.(el.id, "sendBackward")
|
|
752
|
-
)
|
|
753
|
-
}
|
|
754
|
-
>
|
|
755
|
-
{t("panel.config.sendAllBackward")}
|
|
756
|
-
</button>
|
|
757
|
-
<button
|
|
758
|
-
type="button"
|
|
759
|
-
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
760
|
-
onClick={() =>
|
|
761
|
-
effectiveSelectedElements.forEach((el) =>
|
|
762
|
-
updateElement(el.id, { zIndex: 1 })
|
|
763
|
-
)
|
|
764
|
-
}
|
|
765
|
-
>
|
|
766
|
-
{t("panel.config.setAllZIndex1")}
|
|
767
|
-
</button>
|
|
768
|
-
<button
|
|
769
|
-
type="button"
|
|
770
|
-
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
771
|
-
onClick={() =>
|
|
772
|
-
effectiveSelectedElements.forEach((el) =>
|
|
773
|
-
updateElement(el.id, {
|
|
774
|
-
style: {
|
|
775
|
-
...(el.style ?? {}),
|
|
776
|
-
backgroundColor: "#3b82f6",
|
|
777
|
-
},
|
|
778
|
-
})
|
|
779
|
-
)
|
|
780
|
-
}
|
|
781
|
-
>
|
|
782
|
-
{t("panel.config.setAllBgBlue")}
|
|
783
|
-
</button>
|
|
784
|
-
</div>
|
|
785
|
-
</CardContent>
|
|
786
|
-
</Card>
|
|
787
|
-
{effectiveSelectedElements
|
|
788
|
-
.filter((el) => {
|
|
789
|
-
if (!hasSearch) return true;
|
|
790
|
-
const text = `${el.name ?? ""} ${el.id} ${el.materialType ?? ""} zIndex style layer`
|
|
791
|
-
.toLowerCase();
|
|
792
|
-
return text.includes(normalizedSearch);
|
|
793
|
-
})
|
|
794
|
-
.map((el) => (
|
|
795
|
-
<Card
|
|
796
|
-
key={el.id}
|
|
797
|
-
className={el.locked ? "border-amber-500/40 bg-amber-500/5" : ""}
|
|
371
|
+
{showScopePanel ? <ViewElementScopePanel scope={viewElementScope} /> : null}
|
|
372
|
+
<ScopeTemplateWarningsPanel />
|
|
373
|
+
<div
|
|
374
|
+
className={
|
|
375
|
+
showScopePanel || scopeWarnings.length > 0
|
|
376
|
+
? "mt-2 border-t border-border/50 pt-2"
|
|
377
|
+
: "mt-0"
|
|
378
|
+
}
|
|
379
|
+
>
|
|
380
|
+
<div className="flex items-center justify-end gap-2">
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
|
|
384
|
+
onClick={() => setIsSearchCollapsed((prev) => !prev)}
|
|
798
385
|
>
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
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"
|
|
815
|
-
onClick={() => onExcludeSelectedNode?.(el.id)}
|
|
816
|
-
title={t("panel.config.removeFromSelectionTitle")}
|
|
817
|
-
>
|
|
818
|
-
{t("panel.config.removeFromSelection")}
|
|
819
|
-
</button>
|
|
386
|
+
{isSearchCollapsed ? t("panel.config.expandSearch") : t("panel.config.collapseSearch")}
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
{!isSearchCollapsed ? (
|
|
390
|
+
<div className="mt-2">
|
|
391
|
+
<Input
|
|
392
|
+
value={configSearch}
|
|
393
|
+
onChange={(e) => setConfigSearch(e.target.value)}
|
|
394
|
+
placeholder={t("panel.config.searchPlaceholder")}
|
|
395
|
+
className="h-7"
|
|
396
|
+
data-scope-autocomplete="off"
|
|
397
|
+
/>
|
|
398
|
+
{hasSearch ? (
|
|
399
|
+
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
400
|
+
{t("panel.config.searching", { query: configSearch })}
|
|
820
401
|
</div>
|
|
821
|
-
</CardHeader>
|
|
822
|
-
{isNodeCardExpanded(el.id) ? (
|
|
823
|
-
<CardContent className="space-y-2 text-xs">
|
|
824
|
-
<label className="flex items-center gap-2">
|
|
825
|
-
<Checkbox
|
|
826
|
-
checked={el.locked === true}
|
|
827
|
-
onCheckedChange={(checked) => updateElement(el.id, { locked: checked === true })}
|
|
828
|
-
/>
|
|
829
|
-
<span>{t("panel.config.lockedNode")}</span>
|
|
830
|
-
</label>
|
|
831
|
-
{el.locked ? (
|
|
832
|
-
<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">
|
|
833
|
-
{t("panel.config.lockedNodeHint")}
|
|
834
|
-
</div>
|
|
835
|
-
) : null}
|
|
836
|
-
<fieldset disabled={el.locked} className={el.locked ? "opacity-60" : ""}>
|
|
837
|
-
<div className="space-y-2">
|
|
838
|
-
<label className="block space-y-1">
|
|
839
|
-
<div>{t("panel.config.name")}</div>
|
|
840
|
-
<Input
|
|
841
|
-
className="h-7"
|
|
842
|
-
value={el.name ?? ""}
|
|
843
|
-
onChange={(e) => updateElement(el.id, { name: e.target.value || undefined })}
|
|
844
|
-
/>
|
|
845
|
-
</label>
|
|
846
|
-
<div className="grid grid-cols-2 gap-2">
|
|
847
|
-
<label className="block space-y-1">
|
|
848
|
-
<div>zIndex</div>
|
|
849
|
-
<Input
|
|
850
|
-
className="h-7"
|
|
851
|
-
type="number"
|
|
852
|
-
value={el.zIndex ?? 1}
|
|
853
|
-
onChange={(e) =>
|
|
854
|
-
updateElement(el.id, { zIndex: Number(e.target.value) || 1 })
|
|
855
|
-
}
|
|
856
|
-
/>
|
|
857
|
-
</label>
|
|
858
|
-
<label className="block space-y-1">
|
|
859
|
-
<div>{t("panel.config.layer")}</div>
|
|
860
|
-
<Select
|
|
861
|
-
value={el.layerId}
|
|
862
|
-
onValueChange={(value) => updateElement(el.id, { layerId: value })}
|
|
863
|
-
>
|
|
864
|
-
<SelectTrigger className="h-7">
|
|
865
|
-
<SelectValue placeholder={t("panel.config.selectLayer")} />
|
|
866
|
-
</SelectTrigger>
|
|
867
|
-
<SelectContent>
|
|
868
|
-
{layers.map((layer) => (
|
|
869
|
-
<SelectItem key={layer.id} value={layer.id}>
|
|
870
|
-
{layer.name}
|
|
871
|
-
</SelectItem>
|
|
872
|
-
))}
|
|
873
|
-
</SelectContent>
|
|
874
|
-
</Select>
|
|
875
|
-
</label>
|
|
876
|
-
</div>
|
|
877
|
-
<div className="grid grid-cols-2 gap-2">
|
|
878
|
-
<label className="block space-y-1">
|
|
879
|
-
<div>X</div>
|
|
880
|
-
<Input
|
|
881
|
-
className="h-7"
|
|
882
|
-
type="number"
|
|
883
|
-
value={el.x}
|
|
884
|
-
onChange={(e) => updateElement(el.id, { x: Number(e.target.value) || 0 })}
|
|
885
|
-
/>
|
|
886
|
-
</label>
|
|
887
|
-
<label className="block space-y-1">
|
|
888
|
-
<div>Y</div>
|
|
889
|
-
<Input
|
|
890
|
-
className="h-7"
|
|
891
|
-
type="number"
|
|
892
|
-
value={el.y}
|
|
893
|
-
onChange={(e) => updateElement(el.id, { y: Number(e.target.value) || 0 })}
|
|
894
|
-
/>
|
|
895
|
-
</label>
|
|
896
|
-
<label className="block space-y-1">
|
|
897
|
-
<div>{t("panel.config.rotate")}</div>
|
|
898
|
-
<Input
|
|
899
|
-
className="h-7"
|
|
900
|
-
type="number"
|
|
901
|
-
value={el.rotate ?? 0}
|
|
902
|
-
onChange={(e) =>
|
|
903
|
-
updateElement(el.id, { rotate: Number(e.target.value) || 0 })
|
|
904
|
-
}
|
|
905
|
-
/>
|
|
906
|
-
</label>
|
|
907
|
-
<label className="block space-y-1">
|
|
908
|
-
<div>{t("panel.config.width")}</div>
|
|
909
|
-
<Input
|
|
910
|
-
className="h-7"
|
|
911
|
-
type="number"
|
|
912
|
-
min={1}
|
|
913
|
-
value={el.width}
|
|
914
|
-
onChange={(e) =>
|
|
915
|
-
updateElement(el.id, { width: Math.max(1, Number(e.target.value) || 1) })
|
|
916
|
-
}
|
|
917
|
-
/>
|
|
918
|
-
</label>
|
|
919
|
-
<label className="block space-y-1">
|
|
920
|
-
<div>{t("panel.config.height")}</div>
|
|
921
|
-
<Input
|
|
922
|
-
className="h-7"
|
|
923
|
-
type="number"
|
|
924
|
-
min={1}
|
|
925
|
-
value={el.height}
|
|
926
|
-
onChange={(e) =>
|
|
927
|
-
updateElement(el.id, { height: Math.max(1, Number(e.target.value) || 1) })
|
|
928
|
-
}
|
|
929
|
-
/>
|
|
930
|
-
</label>
|
|
931
|
-
</div>
|
|
932
|
-
<div className="grid grid-cols-2 gap-2">
|
|
933
|
-
<label className="block space-y-1">
|
|
934
|
-
<div>{t("panel.config.backgroundColor")}</div>
|
|
935
|
-
<Input
|
|
936
|
-
className="h-7"
|
|
937
|
-
value={el.style?.backgroundColor ?? ""}
|
|
938
|
-
placeholder="#000000"
|
|
939
|
-
onChange={(e) =>
|
|
940
|
-
updateElement(el.id, {
|
|
941
|
-
style: {
|
|
942
|
-
...(el.style ?? {}),
|
|
943
|
-
backgroundColor: e.target.value || undefined,
|
|
944
|
-
},
|
|
945
|
-
})
|
|
946
|
-
}
|
|
947
|
-
/>
|
|
948
|
-
</label>
|
|
949
|
-
<label className="block space-y-1">
|
|
950
|
-
<div>{t("panel.config.borderColorShort")}</div>
|
|
951
|
-
<Input
|
|
952
|
-
className="h-7"
|
|
953
|
-
value={el.style?.borderColor ?? ""}
|
|
954
|
-
placeholder="#000000"
|
|
955
|
-
onChange={(e) =>
|
|
956
|
-
updateElement(el.id, {
|
|
957
|
-
style: {
|
|
958
|
-
...(el.style ?? {}),
|
|
959
|
-
borderColor: e.target.value || undefined,
|
|
960
|
-
},
|
|
961
|
-
})
|
|
962
|
-
}
|
|
963
|
-
/>
|
|
964
|
-
</label>
|
|
965
|
-
</div>
|
|
966
|
-
{CHART_TYPES.has(el.materialType ?? "") ? (
|
|
967
|
-
<div className="grid grid-cols-2 gap-2">
|
|
968
|
-
<label className="block space-y-1 col-span-2">
|
|
969
|
-
<div>{t("panel.config.chartTitle")}</div>
|
|
970
|
-
<Input
|
|
971
|
-
className="h-7"
|
|
972
|
-
value={el.chart?.title ?? ""}
|
|
973
|
-
onChange={(e) =>
|
|
974
|
-
updateElement(el.id, {
|
|
975
|
-
chart: { ...(el.chart ?? {}), title: e.target.value },
|
|
976
|
-
})
|
|
977
|
-
}
|
|
978
|
-
/>
|
|
979
|
-
</label>
|
|
980
|
-
<label className="block space-y-1">
|
|
981
|
-
<div>{t("panel.config.primaryColor")}</div>
|
|
982
|
-
<Input
|
|
983
|
-
className="h-7"
|
|
984
|
-
value={el.chart?.color ?? ""}
|
|
985
|
-
onChange={(e) =>
|
|
986
|
-
updateElement(el.id, {
|
|
987
|
-
chart: { ...(el.chart ?? {}), color: e.target.value || undefined },
|
|
988
|
-
})
|
|
989
|
-
}
|
|
990
|
-
/>
|
|
991
|
-
</label>
|
|
992
|
-
<label className="flex items-center gap-2">
|
|
993
|
-
<Checkbox
|
|
994
|
-
checked={el.chart?.colorMode === "gradient"}
|
|
995
|
-
onCheckedChange={(checked) =>
|
|
996
|
-
updateElement(el.id, {
|
|
997
|
-
chart: { ...(el.chart ?? {}), colorMode: checked ? "gradient" : "solid" },
|
|
998
|
-
})
|
|
999
|
-
}
|
|
1000
|
-
/>
|
|
1001
|
-
<span>{t("panel.config.primaryGradient")}</span>
|
|
1002
|
-
</label>
|
|
1003
|
-
{el.chart?.colorMode === "gradient" ? (
|
|
1004
|
-
<>
|
|
1005
|
-
<label className="block space-y-1">
|
|
1006
|
-
<div>{t("panel.config.gradientFrom")}</div>
|
|
1007
|
-
<Input
|
|
1008
|
-
className="h-7"
|
|
1009
|
-
value={el.chart?.gradientFrom ?? el.chart?.color ?? "#3b82f6"}
|
|
1010
|
-
onChange={(e) =>
|
|
1011
|
-
updateElement(el.id, {
|
|
1012
|
-
chart: { ...(el.chart ?? {}), gradientFrom: e.target.value || "#3b82f6" },
|
|
1013
|
-
})
|
|
1014
|
-
}
|
|
1015
|
-
/>
|
|
1016
|
-
</label>
|
|
1017
|
-
<label className="block space-y-1">
|
|
1018
|
-
<div>{t("panel.config.gradientTo")}</div>
|
|
1019
|
-
<Input
|
|
1020
|
-
className="h-7"
|
|
1021
|
-
value={el.chart?.gradientTo ?? "#22d3ee"}
|
|
1022
|
-
onChange={(e) =>
|
|
1023
|
-
updateElement(el.id, {
|
|
1024
|
-
chart: { ...(el.chart ?? {}), gradientTo: e.target.value || "#22d3ee" },
|
|
1025
|
-
})
|
|
1026
|
-
}
|
|
1027
|
-
/>
|
|
1028
|
-
</label>
|
|
1029
|
-
<label className="block space-y-1 col-span-2">
|
|
1030
|
-
<div>{t("panel.config.gradientDirection")}</div>
|
|
1031
|
-
<Select
|
|
1032
|
-
value={el.chart?.gradientDirection ?? "to-right"}
|
|
1033
|
-
onValueChange={(value) =>
|
|
1034
|
-
updateElement(el.id, {
|
|
1035
|
-
chart: {
|
|
1036
|
-
...(el.chart ?? {}),
|
|
1037
|
-
gradientDirection: value as
|
|
1038
|
-
| "to-right"
|
|
1039
|
-
| "to-bottom"
|
|
1040
|
-
| "to-bottom-right"
|
|
1041
|
-
| "to-top-right",
|
|
1042
|
-
},
|
|
1043
|
-
})
|
|
1044
|
-
}
|
|
1045
|
-
>
|
|
1046
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1047
|
-
<SelectContent>
|
|
1048
|
-
<SelectItem value="to-right">{t("panel.config.dirToRight")}</SelectItem>
|
|
1049
|
-
<SelectItem value="to-bottom">{t("panel.config.dirToBottom")}</SelectItem>
|
|
1050
|
-
<SelectItem value="to-bottom-right">{t("panel.config.dirToBottomRight")}</SelectItem>
|
|
1051
|
-
<SelectItem value="to-top-right">{t("panel.config.dirToTopRight")}</SelectItem>
|
|
1052
|
-
</SelectContent>
|
|
1053
|
-
</Select>
|
|
1054
|
-
</label>
|
|
1055
|
-
<div className="col-span-2 space-y-1">
|
|
1056
|
-
<div className="text-[11px] text-muted-foreground">{t("panel.config.gradientPreview")}</div>
|
|
1057
|
-
<div
|
|
1058
|
-
className="h-6 rounded border border-border/60"
|
|
1059
|
-
style={{
|
|
1060
|
-
backgroundImage: `linear-gradient(${
|
|
1061
|
-
(el.chart?.gradientDirection ?? "to-right") === "to-bottom"
|
|
1062
|
-
? "to bottom"
|
|
1063
|
-
: (el.chart?.gradientDirection ?? "to-right") === "to-bottom-right"
|
|
1064
|
-
? "to bottom right"
|
|
1065
|
-
: (el.chart?.gradientDirection ?? "to-right") === "to-top-right"
|
|
1066
|
-
? "to top right"
|
|
1067
|
-
: "to right"
|
|
1068
|
-
}, ${el.chart?.gradientFrom ?? el.chart?.color ?? "#3b82f6"}, ${el.chart?.gradientTo ?? "#22d3ee"})`,
|
|
1069
|
-
}}
|
|
1070
|
-
/>
|
|
1071
|
-
</div>
|
|
1072
|
-
</>
|
|
1073
|
-
) : null}
|
|
1074
|
-
<label className="block space-y-1">
|
|
1075
|
-
<div>{t("panel.config.render")}</div>
|
|
1076
|
-
<Select
|
|
1077
|
-
value={el.chart?.renderMode ?? "canvas"}
|
|
1078
|
-
onValueChange={(value) =>
|
|
1079
|
-
updateElement(el.id, {
|
|
1080
|
-
chart: { ...(el.chart ?? {}), renderMode: value as "canvas" | "svg" },
|
|
1081
|
-
})
|
|
1082
|
-
}
|
|
1083
|
-
>
|
|
1084
|
-
<SelectTrigger className="h-7">
|
|
1085
|
-
<SelectValue />
|
|
1086
|
-
</SelectTrigger>
|
|
1087
|
-
<SelectContent>
|
|
1088
|
-
<SelectItem value="canvas">canvas</SelectItem>
|
|
1089
|
-
<SelectItem value="svg">svg</SelectItem>
|
|
1090
|
-
</SelectContent>
|
|
1091
|
-
</Select>
|
|
1092
|
-
</label>
|
|
1093
|
-
<label className="block space-y-1">
|
|
1094
|
-
<div>{t("panel.config.tooltipBg")}</div>
|
|
1095
|
-
<Input
|
|
1096
|
-
className="h-7"
|
|
1097
|
-
value={el.chart?.tooltipBackgroundColor ?? ""}
|
|
1098
|
-
placeholder="#0f172a"
|
|
1099
|
-
onChange={(e) =>
|
|
1100
|
-
updateElement(el.id, {
|
|
1101
|
-
chart: {
|
|
1102
|
-
...(el.chart ?? {}),
|
|
1103
|
-
tooltipBackgroundColor: e.target.value || undefined,
|
|
1104
|
-
},
|
|
1105
|
-
})
|
|
1106
|
-
}
|
|
1107
|
-
/>
|
|
1108
|
-
</label>
|
|
1109
|
-
<label className="flex items-center gap-2">
|
|
1110
|
-
<Checkbox
|
|
1111
|
-
checked={el.chart?.tooltipShow ?? true}
|
|
1112
|
-
onCheckedChange={(checked) =>
|
|
1113
|
-
updateElement(el.id, {
|
|
1114
|
-
chart: { ...(el.chart ?? {}), tooltipShow: checked === true },
|
|
1115
|
-
})
|
|
1116
|
-
}
|
|
1117
|
-
/>
|
|
1118
|
-
<span>{t("panel.config.showTooltip")}</span>
|
|
1119
|
-
</label>
|
|
1120
|
-
<label className="block space-y-1">
|
|
1121
|
-
<div>{t("panel.config.tooltipTrigger")}</div>
|
|
1122
|
-
<Select
|
|
1123
|
-
value={el.chart?.tooltipTrigger ?? "axis"}
|
|
1124
|
-
onValueChange={(value) =>
|
|
1125
|
-
updateElement(el.id, {
|
|
1126
|
-
chart: { ...(el.chart ?? {}), tooltipTrigger: value as "axis" | "item" },
|
|
1127
|
-
})
|
|
1128
|
-
}
|
|
1129
|
-
>
|
|
1130
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1131
|
-
<SelectContent>
|
|
1132
|
-
<SelectItem value="axis">axis</SelectItem>
|
|
1133
|
-
<SelectItem value="item">item</SelectItem>
|
|
1134
|
-
</SelectContent>
|
|
1135
|
-
</Select>
|
|
1136
|
-
</label>
|
|
1137
|
-
<label className="block space-y-1">
|
|
1138
|
-
<div>{t("panel.config.tooltipTextColor")}</div>
|
|
1139
|
-
<Input
|
|
1140
|
-
className="h-7"
|
|
1141
|
-
value={el.chart?.tooltipTextColor ?? ""}
|
|
1142
|
-
placeholder="#f8fafc"
|
|
1143
|
-
onChange={(e) =>
|
|
1144
|
-
updateElement(el.id, {
|
|
1145
|
-
chart: {
|
|
1146
|
-
...(el.chart ?? {}),
|
|
1147
|
-
tooltipTextColor: e.target.value || undefined,
|
|
1148
|
-
},
|
|
1149
|
-
})
|
|
1150
|
-
}
|
|
1151
|
-
/>
|
|
1152
|
-
</label>
|
|
1153
|
-
<label className="block space-y-1 col-span-2">
|
|
1154
|
-
{renderFormatterLabel("Tooltip Formatter")}
|
|
1155
|
-
<Input
|
|
1156
|
-
className="h-7"
|
|
1157
|
-
value={el.chart?.tooltipFormatter ?? ""}
|
|
1158
|
-
placeholder="{b}: {c}"
|
|
1159
|
-
onChange={(e) =>
|
|
1160
|
-
updateElement(el.id, {
|
|
1161
|
-
chart: {
|
|
1162
|
-
...(el.chart ?? {}),
|
|
1163
|
-
tooltipFormatter: e.target.value || undefined,
|
|
1164
|
-
},
|
|
1165
|
-
})
|
|
1166
|
-
}
|
|
1167
|
-
/>
|
|
1168
|
-
</label>
|
|
1169
|
-
<label className="block space-y-1 col-span-2">
|
|
1170
|
-
<div>{t("panel.config.labelsCsv")}</div>
|
|
1171
|
-
<Input
|
|
1172
|
-
className="h-7"
|
|
1173
|
-
value={getChartLabelsDisplayText(el.chart)}
|
|
1174
|
-
onChange={(e) =>
|
|
1175
|
-
updateElement(el.id, {
|
|
1176
|
-
chart: {
|
|
1177
|
-
...(el.chart ?? {}),
|
|
1178
|
-
labelsText: e.target.value,
|
|
1179
|
-
},
|
|
1180
|
-
})
|
|
1181
|
-
}
|
|
1182
|
-
/>
|
|
1183
|
-
</label>
|
|
1184
|
-
<label className="block space-y-1 col-span-2">
|
|
1185
|
-
<div>{t("panel.config.valuesCsv")}</div>
|
|
1186
|
-
<Input
|
|
1187
|
-
className="h-7"
|
|
1188
|
-
value={getChartValuesDisplayText(el.chart)}
|
|
1189
|
-
onChange={(e) =>
|
|
1190
|
-
updateElement(el.id, {
|
|
1191
|
-
chart: {
|
|
1192
|
-
...(el.chart ?? {}),
|
|
1193
|
-
valuesText: e.target.value,
|
|
1194
|
-
},
|
|
1195
|
-
})
|
|
1196
|
-
}
|
|
1197
|
-
/>
|
|
1198
|
-
</label>
|
|
1199
|
-
{["bar", "line", "area", "scatter"].includes(el.materialType ?? "") ? (
|
|
1200
|
-
<>
|
|
1201
|
-
<label className="block space-y-1">
|
|
1202
|
-
<div>{t("panel.config.xAxisName")}</div>
|
|
1203
|
-
<Input
|
|
1204
|
-
className="h-7"
|
|
1205
|
-
value={el.chart?.xAxisName ?? ""}
|
|
1206
|
-
onChange={(e) =>
|
|
1207
|
-
updateElement(el.id, {
|
|
1208
|
-
chart: { ...(el.chart ?? {}), xAxisName: e.target.value },
|
|
1209
|
-
})
|
|
1210
|
-
}
|
|
1211
|
-
/>
|
|
1212
|
-
</label>
|
|
1213
|
-
<label className="block space-y-1">
|
|
1214
|
-
<div>{t("panel.config.yAxisName")}</div>
|
|
1215
|
-
<Input
|
|
1216
|
-
className="h-7"
|
|
1217
|
-
value={el.chart?.yAxisName ?? ""}
|
|
1218
|
-
onChange={(e) =>
|
|
1219
|
-
updateElement(el.id, {
|
|
1220
|
-
chart: { ...(el.chart ?? {}), yAxisName: e.target.value },
|
|
1221
|
-
})
|
|
1222
|
-
}
|
|
1223
|
-
/>
|
|
1224
|
-
</label>
|
|
1225
|
-
<label className="block space-y-1">
|
|
1226
|
-
<div>{t("panel.config.xAxisLabelColor")}</div>
|
|
1227
|
-
<Input
|
|
1228
|
-
className="h-7"
|
|
1229
|
-
value={el.chart?.xAxisLabelColor ?? ""}
|
|
1230
|
-
placeholder="#64748b"
|
|
1231
|
-
onChange={(e) =>
|
|
1232
|
-
updateElement(el.id, {
|
|
1233
|
-
chart: { ...(el.chart ?? {}), xAxisLabelColor: e.target.value || undefined },
|
|
1234
|
-
})
|
|
1235
|
-
}
|
|
1236
|
-
/>
|
|
1237
|
-
</label>
|
|
1238
|
-
<label className="block space-y-1">
|
|
1239
|
-
<div>{t("panel.config.yAxisLabelColor")}</div>
|
|
1240
|
-
<Input
|
|
1241
|
-
className="h-7"
|
|
1242
|
-
value={el.chart?.yAxisLabelColor ?? ""}
|
|
1243
|
-
placeholder="#64748b"
|
|
1244
|
-
onChange={(e) =>
|
|
1245
|
-
updateElement(el.id, {
|
|
1246
|
-
chart: { ...(el.chart ?? {}), yAxisLabelColor: e.target.value || undefined },
|
|
1247
|
-
})
|
|
1248
|
-
}
|
|
1249
|
-
/>
|
|
1250
|
-
</label>
|
|
1251
|
-
<label className="block space-y-1">
|
|
1252
|
-
<div>{t("panel.config.xAxisLabelFontSize")}</div>
|
|
1253
|
-
<Input
|
|
1254
|
-
className="h-7"
|
|
1255
|
-
type="number"
|
|
1256
|
-
min={8}
|
|
1257
|
-
max={48}
|
|
1258
|
-
value={el.chart?.xAxisLabelFontSize ?? 10}
|
|
1259
|
-
onChange={(e) =>
|
|
1260
|
-
updateElement(el.id, {
|
|
1261
|
-
chart: {
|
|
1262
|
-
...(el.chart ?? {}),
|
|
1263
|
-
xAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
|
|
1264
|
-
},
|
|
1265
|
-
})
|
|
1266
|
-
}
|
|
1267
|
-
/>
|
|
1268
|
-
</label>
|
|
1269
|
-
<label className="block space-y-1">
|
|
1270
|
-
<div>{t("panel.config.yAxisLabelFontSize")}</div>
|
|
1271
|
-
<Input
|
|
1272
|
-
className="h-7"
|
|
1273
|
-
type="number"
|
|
1274
|
-
min={8}
|
|
1275
|
-
max={48}
|
|
1276
|
-
value={el.chart?.yAxisLabelFontSize ?? 10}
|
|
1277
|
-
onChange={(e) =>
|
|
1278
|
-
updateElement(el.id, {
|
|
1279
|
-
chart: {
|
|
1280
|
-
...(el.chart ?? {}),
|
|
1281
|
-
yAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
|
|
1282
|
-
},
|
|
1283
|
-
})
|
|
1284
|
-
}
|
|
1285
|
-
/>
|
|
1286
|
-
</label>
|
|
1287
|
-
<label className="flex items-center gap-2">
|
|
1288
|
-
<Checkbox
|
|
1289
|
-
checked={el.chart?.xAxisLabelAutoEllipsis ?? false}
|
|
1290
|
-
onCheckedChange={(checked) =>
|
|
1291
|
-
updateElement(el.id, {
|
|
1292
|
-
chart: { ...(el.chart ?? {}), xAxisLabelAutoEllipsis: checked === true },
|
|
1293
|
-
})
|
|
1294
|
-
}
|
|
1295
|
-
/>
|
|
1296
|
-
<span>{t("panel.config.xAxisLabelAutoEllipsis")}</span>
|
|
1297
|
-
</label>
|
|
1298
|
-
<label className="flex items-center gap-2">
|
|
1299
|
-
<Checkbox
|
|
1300
|
-
checked={el.chart?.yAxisLabelAutoEllipsis ?? false}
|
|
1301
|
-
onCheckedChange={(checked) =>
|
|
1302
|
-
updateElement(el.id, {
|
|
1303
|
-
chart: { ...(el.chart ?? {}), yAxisLabelAutoEllipsis: checked === true },
|
|
1304
|
-
})
|
|
1305
|
-
}
|
|
1306
|
-
/>
|
|
1307
|
-
<span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
|
|
1308
|
-
</label>
|
|
1309
|
-
<label className="flex items-center gap-2">
|
|
1310
|
-
<Checkbox
|
|
1311
|
-
checked={el.chart?.xAxisTickShow ?? true}
|
|
1312
|
-
onCheckedChange={(checked) =>
|
|
1313
|
-
updateElement(el.id, {
|
|
1314
|
-
chart: { ...(el.chart ?? {}), xAxisTickShow: checked === true },
|
|
1315
|
-
})
|
|
1316
|
-
}
|
|
1317
|
-
/>
|
|
1318
|
-
<span>{t("panel.config.xAxisTick")}</span>
|
|
1319
|
-
</label>
|
|
1320
|
-
<label className="flex items-center gap-2">
|
|
1321
|
-
<Checkbox
|
|
1322
|
-
checked={el.chart?.yAxisTickShow ?? true}
|
|
1323
|
-
onCheckedChange={(checked) =>
|
|
1324
|
-
updateElement(el.id, {
|
|
1325
|
-
chart: { ...(el.chart ?? {}), yAxisTickShow: checked === true },
|
|
1326
|
-
})
|
|
1327
|
-
}
|
|
1328
|
-
/>
|
|
1329
|
-
<span>{t("panel.config.yAxisTick")}</span>
|
|
1330
|
-
</label>
|
|
1331
|
-
<label className="block space-y-1">
|
|
1332
|
-
<div>{t("panel.config.xAxisTickColor")}</div>
|
|
1333
|
-
<Input
|
|
1334
|
-
className="h-7"
|
|
1335
|
-
value={el.chart?.xAxisTickColor ?? ""}
|
|
1336
|
-
placeholder="#94a3b8"
|
|
1337
|
-
onChange={(e) =>
|
|
1338
|
-
updateElement(el.id, {
|
|
1339
|
-
chart: { ...(el.chart ?? {}), xAxisTickColor: e.target.value || undefined },
|
|
1340
|
-
})
|
|
1341
|
-
}
|
|
1342
|
-
/>
|
|
1343
|
-
</label>
|
|
1344
|
-
<label className="block space-y-1">
|
|
1345
|
-
<div>{t("panel.config.yAxisTickColor")}</div>
|
|
1346
|
-
<Input
|
|
1347
|
-
className="h-7"
|
|
1348
|
-
value={el.chart?.yAxisTickColor ?? ""}
|
|
1349
|
-
placeholder="#94a3b8"
|
|
1350
|
-
onChange={(e) =>
|
|
1351
|
-
updateElement(el.id, {
|
|
1352
|
-
chart: { ...(el.chart ?? {}), yAxisTickColor: e.target.value || undefined },
|
|
1353
|
-
})
|
|
1354
|
-
}
|
|
1355
|
-
/>
|
|
1356
|
-
</label>
|
|
1357
|
-
<label className="flex items-center gap-2">
|
|
1358
|
-
<Checkbox
|
|
1359
|
-
checked={el.chart?.xAxisSplitLineShow ?? false}
|
|
1360
|
-
onCheckedChange={(checked) =>
|
|
1361
|
-
updateElement(el.id, {
|
|
1362
|
-
chart: { ...(el.chart ?? {}), xAxisSplitLineShow: checked === true },
|
|
1363
|
-
})
|
|
1364
|
-
}
|
|
1365
|
-
/>
|
|
1366
|
-
<span>{t("panel.config.xAxisSplitLine")}</span>
|
|
1367
|
-
</label>
|
|
1368
|
-
<label className="flex items-center gap-2">
|
|
1369
|
-
<Checkbox
|
|
1370
|
-
checked={el.chart?.yAxisSplitLineShow ?? true}
|
|
1371
|
-
onCheckedChange={(checked) =>
|
|
1372
|
-
updateElement(el.id, {
|
|
1373
|
-
chart: { ...(el.chart ?? {}), yAxisSplitLineShow: checked === true },
|
|
1374
|
-
})
|
|
1375
|
-
}
|
|
1376
|
-
/>
|
|
1377
|
-
<span>{t("panel.config.yAxisSplitLine")}</span>
|
|
1378
|
-
</label>
|
|
1379
|
-
<label className="block space-y-1">
|
|
1380
|
-
<div>{t("panel.config.xAxisSplitLineColor")}</div>
|
|
1381
|
-
<Input
|
|
1382
|
-
className="h-7"
|
|
1383
|
-
value={el.chart?.xAxisSplitLineColor ?? ""}
|
|
1384
|
-
placeholder="#e2e8f0"
|
|
1385
|
-
onChange={(e) =>
|
|
1386
|
-
updateElement(el.id, {
|
|
1387
|
-
chart: { ...(el.chart ?? {}), xAxisSplitLineColor: e.target.value || undefined },
|
|
1388
|
-
})
|
|
1389
|
-
}
|
|
1390
|
-
/>
|
|
1391
|
-
</label>
|
|
1392
|
-
<label className="block space-y-1">
|
|
1393
|
-
<div>{t("panel.config.yAxisSplitLineColor")}</div>
|
|
1394
|
-
<Input
|
|
1395
|
-
className="h-7"
|
|
1396
|
-
value={el.chart?.yAxisSplitLineColor ?? ""}
|
|
1397
|
-
placeholder="#e2e8f0"
|
|
1398
|
-
onChange={(e) =>
|
|
1399
|
-
updateElement(el.id, {
|
|
1400
|
-
chart: { ...(el.chart ?? {}), yAxisSplitLineColor: e.target.value || undefined },
|
|
1401
|
-
})
|
|
1402
|
-
}
|
|
1403
|
-
/>
|
|
1404
|
-
</label>
|
|
1405
|
-
</>
|
|
1406
|
-
) : null}
|
|
1407
|
-
{el.materialType === "gauge" ? (
|
|
1408
|
-
<label className="block space-y-1">
|
|
1409
|
-
<div>{t("panel.config.gaugeValue")}</div>
|
|
1410
|
-
<Input
|
|
1411
|
-
className="h-7"
|
|
1412
|
-
type="number"
|
|
1413
|
-
value={el.chart?.values?.[0] ?? 0}
|
|
1414
|
-
onChange={(e) =>
|
|
1415
|
-
updateElement(el.id, {
|
|
1416
|
-
chart: {
|
|
1417
|
-
...(el.chart ?? {}),
|
|
1418
|
-
values: [Number(e.target.value) || 0],
|
|
1419
|
-
},
|
|
1420
|
-
})
|
|
1421
|
-
}
|
|
1422
|
-
/>
|
|
1423
|
-
</label>
|
|
1424
|
-
) : null}
|
|
1425
|
-
{el.materialType === "bar" ? (
|
|
1426
|
-
<label className="block space-y-1">
|
|
1427
|
-
<div>{t("panel.config.barWidth")}</div>
|
|
1428
|
-
<Input
|
|
1429
|
-
className="h-7"
|
|
1430
|
-
type="number"
|
|
1431
|
-
min={1}
|
|
1432
|
-
placeholder={t("panel.config.barWidthAuto")}
|
|
1433
|
-
value={el.chart?.barWidth ?? ""}
|
|
1434
|
-
onChange={(e) => {
|
|
1435
|
-
const raw = e.target.value.trim();
|
|
1436
|
-
updateElement(el.id, {
|
|
1437
|
-
chart: {
|
|
1438
|
-
...(el.chart ?? {}),
|
|
1439
|
-
barWidth: raw
|
|
1440
|
-
? Math.max(1, Number(raw) || 1)
|
|
1441
|
-
: undefined,
|
|
1442
|
-
},
|
|
1443
|
-
});
|
|
1444
|
-
}}
|
|
1445
|
-
/>
|
|
1446
|
-
</label>
|
|
1447
|
-
) : null}
|
|
1448
|
-
{(el.materialType === "line" || el.materialType === "area") ? (
|
|
1449
|
-
<label className="flex items-center gap-2">
|
|
1450
|
-
<Checkbox
|
|
1451
|
-
checked={el.chart?.smooth ?? true}
|
|
1452
|
-
onCheckedChange={(checked) =>
|
|
1453
|
-
updateElement(el.id, {
|
|
1454
|
-
chart: { ...(el.chart ?? {}), smooth: checked === true },
|
|
1455
|
-
})
|
|
1456
|
-
}
|
|
1457
|
-
/>
|
|
1458
|
-
<span>{t("panel.config.smooth")}</span>
|
|
1459
|
-
</label>
|
|
1460
|
-
) : null}
|
|
1461
|
-
{el.materialType === "pie" ? (
|
|
1462
|
-
<>
|
|
1463
|
-
<label className="block space-y-1">
|
|
1464
|
-
<div>{t("panel.config.pieInnerRadius")}</div>
|
|
1465
|
-
<Input
|
|
1466
|
-
className="h-7"
|
|
1467
|
-
type="number"
|
|
1468
|
-
min={0}
|
|
1469
|
-
max={99}
|
|
1470
|
-
value={el.chart?.pieInnerRadius ?? 30}
|
|
1471
|
-
onChange={(e) =>
|
|
1472
|
-
updateElement(el.id, {
|
|
1473
|
-
chart: {
|
|
1474
|
-
...(el.chart ?? {}),
|
|
1475
|
-
pieInnerRadius: Math.max(0, Math.min(99, Number(e.target.value) || 0)),
|
|
1476
|
-
},
|
|
1477
|
-
})
|
|
1478
|
-
}
|
|
1479
|
-
/>
|
|
1480
|
-
</label>
|
|
1481
|
-
<label className="block space-y-1">
|
|
1482
|
-
<div>{t("panel.config.pieOuterRadius")}</div>
|
|
1483
|
-
<Input
|
|
1484
|
-
className="h-7"
|
|
1485
|
-
type="number"
|
|
1486
|
-
min={1}
|
|
1487
|
-
max={100}
|
|
1488
|
-
value={el.chart?.pieOuterRadius ?? 65}
|
|
1489
|
-
onChange={(e) =>
|
|
1490
|
-
updateElement(el.id, {
|
|
1491
|
-
chart: {
|
|
1492
|
-
...(el.chart ?? {}),
|
|
1493
|
-
pieOuterRadius: Math.max(1, Math.min(100, Number(e.target.value) || 1)),
|
|
1494
|
-
},
|
|
1495
|
-
})
|
|
1496
|
-
}
|
|
1497
|
-
/>
|
|
1498
|
-
</label>
|
|
1499
|
-
</>
|
|
1500
|
-
) : null}
|
|
1501
|
-
<label className="block space-y-1">
|
|
1502
|
-
{renderOptionLabel(t("panel.config.gridLeft"), "grid.left", t("panel.config.gridLeftHint"))}
|
|
1503
|
-
<Input
|
|
1504
|
-
className="h-7"
|
|
1505
|
-
type="number"
|
|
1506
|
-
value={Number((el.chart?.option as any)?.grid?.left ?? 28)}
|
|
1507
|
-
onChange={(e) =>
|
|
1508
|
-
updateElement(el.id, {
|
|
1509
|
-
chart: {
|
|
1510
|
-
...(el.chart ?? {}),
|
|
1511
|
-
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1512
|
-
grid: { left: Number(e.target.value) || 0 },
|
|
1513
|
-
}),
|
|
1514
|
-
},
|
|
1515
|
-
})
|
|
1516
|
-
}
|
|
1517
|
-
/>
|
|
1518
|
-
</label>
|
|
1519
|
-
<label className="block space-y-1">
|
|
1520
|
-
{renderOptionLabel(t("panel.config.gridRight"), "grid.right", t("panel.config.gridRightHint"))}
|
|
1521
|
-
<Input
|
|
1522
|
-
className="h-7"
|
|
1523
|
-
type="number"
|
|
1524
|
-
value={Number((el.chart?.option as any)?.grid?.right ?? 10)}
|
|
1525
|
-
onChange={(e) =>
|
|
1526
|
-
updateElement(el.id, {
|
|
1527
|
-
chart: {
|
|
1528
|
-
...(el.chart ?? {}),
|
|
1529
|
-
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1530
|
-
grid: { right: Number(e.target.value) || 0 },
|
|
1531
|
-
}),
|
|
1532
|
-
},
|
|
1533
|
-
})
|
|
1534
|
-
}
|
|
1535
|
-
/>
|
|
1536
|
-
</label>
|
|
1537
|
-
<label className="block space-y-1">
|
|
1538
|
-
{renderOptionLabel(t("panel.config.gridTop"), "grid.top", t("panel.config.gridTopHint"))}
|
|
1539
|
-
<Input
|
|
1540
|
-
className="h-7"
|
|
1541
|
-
type="number"
|
|
1542
|
-
value={Number((el.chart?.option as any)?.grid?.top ?? 30)}
|
|
1543
|
-
onChange={(e) =>
|
|
1544
|
-
updateElement(el.id, {
|
|
1545
|
-
chart: {
|
|
1546
|
-
...(el.chart ?? {}),
|
|
1547
|
-
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1548
|
-
grid: { top: Number(e.target.value) || 0 },
|
|
1549
|
-
}),
|
|
1550
|
-
},
|
|
1551
|
-
})
|
|
1552
|
-
}
|
|
1553
|
-
/>
|
|
1554
|
-
</label>
|
|
1555
|
-
<label className="block space-y-1">
|
|
1556
|
-
{renderOptionLabel(t("panel.config.gridBottom"), "grid.bottom", t("panel.config.gridBottomHint"))}
|
|
1557
|
-
<Input
|
|
1558
|
-
className="h-7"
|
|
1559
|
-
type="number"
|
|
1560
|
-
value={Number((el.chart?.option as any)?.grid?.bottom ?? 20)}
|
|
1561
|
-
onChange={(e) =>
|
|
1562
|
-
updateElement(el.id, {
|
|
1563
|
-
chart: {
|
|
1564
|
-
...(el.chart ?? {}),
|
|
1565
|
-
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1566
|
-
grid: { bottom: Number(e.target.value) || 0 },
|
|
1567
|
-
}),
|
|
1568
|
-
},
|
|
1569
|
-
})
|
|
1570
|
-
}
|
|
1571
|
-
/>
|
|
1572
|
-
</label>
|
|
1573
|
-
<label className="block space-y-1">
|
|
1574
|
-
{renderOptionLabel(t("panel.config.legendPosition"), "legend.top", t("panel.config.legendPositionHint"))}
|
|
1575
|
-
<Select
|
|
1576
|
-
value={String((el.chart?.option as any)?.legend?.top ?? "top")}
|
|
1577
|
-
onValueChange={(value) =>
|
|
1578
|
-
updateElement(el.id, {
|
|
1579
|
-
chart: {
|
|
1580
|
-
...(el.chart ?? {}),
|
|
1581
|
-
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1582
|
-
legend: { top: value },
|
|
1583
|
-
}),
|
|
1584
|
-
},
|
|
1585
|
-
})
|
|
1586
|
-
}
|
|
1587
|
-
>
|
|
1588
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1589
|
-
<SelectContent>
|
|
1590
|
-
<SelectItem value="top">top</SelectItem>
|
|
1591
|
-
<SelectItem value="bottom">bottom</SelectItem>
|
|
1592
|
-
<SelectItem value="left">left</SelectItem>
|
|
1593
|
-
<SelectItem value="right">right</SelectItem>
|
|
1594
|
-
</SelectContent>
|
|
1595
|
-
</Select>
|
|
1596
|
-
</label>
|
|
1597
|
-
<label className="block space-y-1">
|
|
1598
|
-
{renderOptionLabel(t("panel.config.legendOrient"), "legend.orient", t("panel.config.legendOrientHint"))}
|
|
1599
|
-
<Select
|
|
1600
|
-
value={String((el.chart?.option as any)?.legend?.orient ?? "horizontal")}
|
|
1601
|
-
onValueChange={(value) =>
|
|
1602
|
-
updateElement(el.id, {
|
|
1603
|
-
chart: {
|
|
1604
|
-
...(el.chart ?? {}),
|
|
1605
|
-
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1606
|
-
legend: { orient: value },
|
|
1607
|
-
}),
|
|
1608
|
-
},
|
|
1609
|
-
})
|
|
1610
|
-
}
|
|
1611
|
-
>
|
|
1612
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1613
|
-
<SelectContent>
|
|
1614
|
-
<SelectItem value="horizontal">horizontal</SelectItem>
|
|
1615
|
-
<SelectItem value="vertical">vertical</SelectItem>
|
|
1616
|
-
</SelectContent>
|
|
1617
|
-
</Select>
|
|
1618
|
-
</label>
|
|
1619
|
-
<label className="flex items-center gap-2">
|
|
1620
|
-
<Checkbox
|
|
1621
|
-
checked={Boolean((el.chart?.option as any)?.legend?.show ?? true)}
|
|
1622
|
-
onCheckedChange={(checked) =>
|
|
1623
|
-
updateElement(el.id, {
|
|
1624
|
-
chart: {
|
|
1625
|
-
...(el.chart ?? {}),
|
|
1626
|
-
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1627
|
-
legend: { show: checked === true },
|
|
1628
|
-
}),
|
|
1629
|
-
},
|
|
1630
|
-
})
|
|
1631
|
-
}
|
|
1632
|
-
/>
|
|
1633
|
-
<span>{t("panel.config.showLegendWithKey")}</span>
|
|
1634
|
-
</label>
|
|
1635
|
-
<label className="flex items-center gap-2">
|
|
1636
|
-
<Checkbox
|
|
1637
|
-
checked={Boolean(
|
|
1638
|
-
Array.isArray((el.chart?.option as any)?.dataZoom) &&
|
|
1639
|
-
(el.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "inside")
|
|
1640
|
-
)}
|
|
1641
|
-
onCheckedChange={(checked) => {
|
|
1642
|
-
const prev = Array.isArray((el.chart?.option as any)?.dataZoom)
|
|
1643
|
-
? [...(el.chart?.option as any).dataZoom]
|
|
1644
|
-
: [];
|
|
1645
|
-
const next = checked
|
|
1646
|
-
? [...prev.filter((z: any) => z?.type !== "inside"), { type: "inside" }]
|
|
1647
|
-
: prev.filter((z: any) => z?.type !== "inside");
|
|
1648
|
-
updateElement(el.id, {
|
|
1649
|
-
chart: {
|
|
1650
|
-
...(el.chart ?? {}),
|
|
1651
|
-
option: mergeOptionPatch(el.chart?.option as any, { dataZoom: next }),
|
|
1652
|
-
},
|
|
1653
|
-
});
|
|
1654
|
-
}}
|
|
1655
|
-
/>
|
|
1656
|
-
<span>{t("panel.config.zoomInsideWithKey")}</span>
|
|
1657
|
-
</label>
|
|
1658
|
-
<label className="flex items-center gap-2">
|
|
1659
|
-
<Checkbox
|
|
1660
|
-
checked={Boolean(
|
|
1661
|
-
Array.isArray((el.chart?.option as any)?.dataZoom) &&
|
|
1662
|
-
(el.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "slider")
|
|
1663
|
-
)}
|
|
1664
|
-
onCheckedChange={(checked) => {
|
|
1665
|
-
const prev = Array.isArray((el.chart?.option as any)?.dataZoom)
|
|
1666
|
-
? [...(el.chart?.option as any).dataZoom]
|
|
1667
|
-
: [];
|
|
1668
|
-
const next = checked
|
|
1669
|
-
? [...prev.filter((z: any) => z?.type !== "slider"), { type: "slider" }]
|
|
1670
|
-
: prev.filter((z: any) => z?.type !== "slider");
|
|
1671
|
-
updateElement(el.id, {
|
|
1672
|
-
chart: {
|
|
1673
|
-
...(el.chart ?? {}),
|
|
1674
|
-
option: mergeOptionPatch(el.chart?.option as any, { dataZoom: next }),
|
|
1675
|
-
},
|
|
1676
|
-
});
|
|
1677
|
-
}}
|
|
1678
|
-
/>
|
|
1679
|
-
<span>{t("panel.config.zoomSliderWithKey")}</span>
|
|
1680
|
-
</label>
|
|
1681
|
-
<label className="flex items-center gap-2">
|
|
1682
|
-
<Checkbox
|
|
1683
|
-
checked={Boolean((el.chart?.option as any)?.axisPointer?.show ?? false)}
|
|
1684
|
-
onCheckedChange={(checked) =>
|
|
1685
|
-
updateElement(el.id, {
|
|
1686
|
-
chart: {
|
|
1687
|
-
...(el.chart ?? {}),
|
|
1688
|
-
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1689
|
-
axisPointer: { show: checked === true },
|
|
1690
|
-
}),
|
|
1691
|
-
},
|
|
1692
|
-
})
|
|
1693
|
-
}
|
|
1694
|
-
/>
|
|
1695
|
-
<span>{t("panel.config.showAxisPointerWithKey")}</span>
|
|
1696
|
-
</label>
|
|
1697
|
-
<label className="block space-y-1">
|
|
1698
|
-
<div>{t("panel.config.axisPointerTypeWithKey")}</div>
|
|
1699
|
-
<Select
|
|
1700
|
-
value={String((el.chart?.option as any)?.axisPointer?.type ?? "line")}
|
|
1701
|
-
onValueChange={(value) =>
|
|
1702
|
-
updateElement(el.id, {
|
|
1703
|
-
chart: {
|
|
1704
|
-
...(el.chart ?? {}),
|
|
1705
|
-
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1706
|
-
axisPointer: { type: value },
|
|
1707
|
-
}),
|
|
1708
|
-
},
|
|
1709
|
-
})
|
|
1710
|
-
}
|
|
1711
|
-
>
|
|
1712
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1713
|
-
<SelectContent>
|
|
1714
|
-
<SelectItem value="line">line</SelectItem>
|
|
1715
|
-
<SelectItem value="shadow">shadow</SelectItem>
|
|
1716
|
-
<SelectItem value="cross">cross</SelectItem>
|
|
1717
|
-
</SelectContent>
|
|
1718
|
-
</Select>
|
|
1719
|
-
</label>
|
|
1720
|
-
<label className="block space-y-1 col-span-2">
|
|
1721
|
-
<div>{t("panel.config.advancedOptionJson")}</div>
|
|
1722
|
-
<Textarea
|
|
1723
|
-
className="h-28 font-mono text-[11px]"
|
|
1724
|
-
defaultValue={JSON.stringify(el.chart?.option ?? {}, null, 2)}
|
|
1725
|
-
placeholder={t("panel.config.advancedJsonPlaceholder")}
|
|
1726
|
-
onBlur={(e) => {
|
|
1727
|
-
const nextText = e.target.value.trim();
|
|
1728
|
-
if (!nextText) {
|
|
1729
|
-
updateElement(el.id, { chart: { ...(el.chart ?? {}), option: undefined } });
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
try {
|
|
1733
|
-
const parsed = JSON.parse(nextText) as Record<string, unknown>;
|
|
1734
|
-
updateElement(el.id, { chart: { ...(el.chart ?? {}), option: parsed } });
|
|
1735
|
-
} catch {
|
|
1736
|
-
// ignore invalid json input on blur
|
|
1737
|
-
}
|
|
1738
|
-
}}
|
|
1739
|
-
/>
|
|
1740
|
-
</label>
|
|
1741
|
-
</div>
|
|
1742
|
-
) : null}
|
|
1743
|
-
{el.materialType === "text" ? (
|
|
1744
|
-
<div className="grid grid-cols-2 gap-2">
|
|
1745
|
-
<label className="block space-y-1 col-span-2">
|
|
1746
|
-
<div>{t("panel.config.textHtml")}</div>
|
|
1747
|
-
<Textarea
|
|
1748
|
-
className="h-24"
|
|
1749
|
-
value={el.textHtml ?? ""}
|
|
1750
|
-
onChange={(e) => updateElement(el.id, { textHtml: e.target.value || "<p><br/></p>" })}
|
|
1751
|
-
/>
|
|
1752
|
-
</label>
|
|
1753
|
-
<label className="block space-y-1">
|
|
1754
|
-
<div>{t("panel.config.fontSize")}</div>
|
|
1755
|
-
<Input
|
|
1756
|
-
className="h-7"
|
|
1757
|
-
type="number"
|
|
1758
|
-
min={8}
|
|
1759
|
-
value={el.textFontSize ?? 14}
|
|
1760
|
-
onChange={(e) => updateElement(el.id, { textFontSize: Math.max(8, Number(e.target.value) || 14) })}
|
|
1761
|
-
/>
|
|
1762
|
-
</label>
|
|
1763
|
-
<label className="block space-y-1">
|
|
1764
|
-
<div>{t("panel.config.textColor")}</div>
|
|
1765
|
-
<Input
|
|
1766
|
-
className="h-7"
|
|
1767
|
-
value={el.textColor ?? ""}
|
|
1768
|
-
onChange={(e) => updateElement(el.id, { textColor: e.target.value || undefined })}
|
|
1769
|
-
/>
|
|
1770
|
-
</label>
|
|
1771
|
-
</div>
|
|
1772
|
-
) : null}
|
|
1773
|
-
{el.materialType === "audio" ? (
|
|
1774
|
-
<div className="grid grid-cols-2 gap-2">
|
|
1775
|
-
<label className="block space-y-1 col-span-2">
|
|
1776
|
-
<div>{t("panel.config.audioUrl")}</div>
|
|
1777
|
-
<Input
|
|
1778
|
-
className="h-7"
|
|
1779
|
-
value={el.audioRemoteUrl ?? ""}
|
|
1780
|
-
onChange={(e) => updateElement(el.id, { audioRemoteUrl: e.target.value || undefined })}
|
|
1781
|
-
/>
|
|
1782
|
-
</label>
|
|
1783
|
-
<label className="block space-y-1">
|
|
1784
|
-
<div>{t("panel.config.effect")}</div>
|
|
1785
|
-
<Select
|
|
1786
|
-
value={el.audioVisualEffect ?? "pulse"}
|
|
1787
|
-
onValueChange={(value) =>
|
|
1788
|
-
updateElement(el.id, { audioVisualEffect: value as "none" | "pulse" | "ripple" })
|
|
1789
|
-
}
|
|
1790
|
-
>
|
|
1791
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1792
|
-
<SelectContent>
|
|
1793
|
-
<SelectItem value="none">none</SelectItem>
|
|
1794
|
-
<SelectItem value="pulse">pulse</SelectItem>
|
|
1795
|
-
<SelectItem value="ripple">ripple</SelectItem>
|
|
1796
|
-
</SelectContent>
|
|
1797
|
-
</Select>
|
|
1798
|
-
</label>
|
|
1799
|
-
<label className="block space-y-1">
|
|
1800
|
-
<div>{t("panel.config.speed")}</div>
|
|
1801
|
-
<Select
|
|
1802
|
-
value={el.audioVisualSpeed ?? "normal"}
|
|
1803
|
-
onValueChange={(value) =>
|
|
1804
|
-
updateElement(el.id, { audioVisualSpeed: value as "slow" | "normal" | "fast" })
|
|
1805
|
-
}
|
|
1806
|
-
>
|
|
1807
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1808
|
-
<SelectContent>
|
|
1809
|
-
<SelectItem value="slow">slow</SelectItem>
|
|
1810
|
-
<SelectItem value="normal">normal</SelectItem>
|
|
1811
|
-
<SelectItem value="fast">fast</SelectItem>
|
|
1812
|
-
</SelectContent>
|
|
1813
|
-
</Select>
|
|
1814
|
-
</label>
|
|
1815
|
-
</div>
|
|
1816
|
-
) : null}
|
|
1817
|
-
{el.materialType === "video" ? (
|
|
1818
|
-
<label className="block space-y-1">
|
|
1819
|
-
<div>{t("panel.config.videoUrl")}</div>
|
|
1820
|
-
<Input
|
|
1821
|
-
className="h-7"
|
|
1822
|
-
value={el.videoRemoteUrl ?? ""}
|
|
1823
|
-
onChange={(e) => updateElement(el.id, { videoRemoteUrl: e.target.value || undefined })}
|
|
1824
|
-
/>
|
|
1825
|
-
</label>
|
|
1826
|
-
) : null}
|
|
1827
|
-
{el.materialType === "grid" ? (
|
|
1828
|
-
<div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
|
|
1829
|
-
<label className="block space-y-1.5">
|
|
1830
|
-
<div>{t("panel.config.rows")}</div>
|
|
1831
|
-
<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) })} />
|
|
1832
|
-
</label>
|
|
1833
|
-
<label className="block space-y-1.5">
|
|
1834
|
-
<div>{t("panel.config.cols")}</div>
|
|
1835
|
-
<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) })} />
|
|
1836
|
-
</label>
|
|
1837
|
-
</div>
|
|
1838
|
-
) : null}
|
|
1839
|
-
{el.materialType === "geometry" ? (
|
|
1840
|
-
<div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
|
|
1841
|
-
<label className="block space-y-1.5">
|
|
1842
|
-
<div>{t("panel.config.shape")}</div>
|
|
1843
|
-
<Select
|
|
1844
|
-
value={el.geometryShape ?? "rect"}
|
|
1845
|
-
onValueChange={(value) =>
|
|
1846
|
-
updateElement(el.id, { geometryShape: value as PanelElement["geometryShape"] })
|
|
1847
|
-
}
|
|
1848
|
-
>
|
|
1849
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1850
|
-
<SelectContent>
|
|
1851
|
-
<SelectItem value="rect">{t("panel.config.shapeRect")}</SelectItem>
|
|
1852
|
-
<SelectItem value="circle">{t("panel.config.shapeCircle")}</SelectItem>
|
|
1853
|
-
<SelectItem value="triangle">{t("panel.config.shapeTriangle")}</SelectItem>
|
|
1854
|
-
<SelectItem value="diamond">{t("panel.config.shapeDiamond")}</SelectItem>
|
|
1855
|
-
<SelectItem value="hexagon">{t("panel.config.shapeHexagon")}</SelectItem>
|
|
1856
|
-
<SelectItem value="star">{t("panel.config.shapeStar")}</SelectItem>
|
|
1857
|
-
<SelectItem value="heart">{t("panel.config.shapeHeart")}</SelectItem>
|
|
1858
|
-
</SelectContent>
|
|
1859
|
-
</Select>
|
|
1860
|
-
</label>
|
|
1861
|
-
<label className="block space-y-1.5">
|
|
1862
|
-
<div>{t("panel.config.color")}</div>
|
|
1863
|
-
<Input
|
|
1864
|
-
className="h-7"
|
|
1865
|
-
value={el.geometryColor ?? "#3b82f6"}
|
|
1866
|
-
onChange={(e) => updateElement(el.id, { geometryColor: e.target.value || "#3b82f6" })}
|
|
1867
|
-
/>
|
|
1868
|
-
</label>
|
|
1869
|
-
</div>
|
|
1870
|
-
) : null}
|
|
1871
|
-
{el.materialType === "reference" || el.materialType === "viewport" ? (
|
|
1872
|
-
<div className="grid grid-cols-2 gap-2">
|
|
1873
|
-
<label className="block space-y-1.5">
|
|
1874
|
-
<div>{t("panel.config.refLayer")}</div>
|
|
1875
|
-
<Select
|
|
1876
|
-
value={el.refLayerId ?? "__none__"}
|
|
1877
|
-
onValueChange={(value) => updateElement(el.id, { refLayerId: value === "__none__" ? undefined : value })}
|
|
1878
|
-
>
|
|
1879
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1880
|
-
<SelectContent>
|
|
1881
|
-
<SelectItem value="__none__">{t("common.none")}</SelectItem>
|
|
1882
|
-
{layers.filter((l) => l.id !== el.layerId).map((l) => (
|
|
1883
|
-
<SelectItem key={l.id} value={l.id}>{l.name}</SelectItem>
|
|
1884
|
-
))}
|
|
1885
|
-
</SelectContent>
|
|
1886
|
-
</Select>
|
|
1887
|
-
</label>
|
|
1888
|
-
<label className="block space-y-1.5">
|
|
1889
|
-
<div>{t("panel.config.copy")}</div>
|
|
1890
|
-
<Select
|
|
1891
|
-
value={el.refCopyMode ?? "shallow"}
|
|
1892
|
-
onValueChange={(value) => setReferenceCopyMode?.(el.id, value as ReferenceCopyMode)}
|
|
1893
|
-
>
|
|
1894
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1895
|
-
<SelectContent>
|
|
1896
|
-
<SelectItem value="shallow">shallow</SelectItem>
|
|
1897
|
-
<SelectItem value="deep">deep</SelectItem>
|
|
1898
|
-
</SelectContent>
|
|
1899
|
-
</Select>
|
|
1900
|
-
</label>
|
|
1901
|
-
</div>
|
|
1902
|
-
) : null}
|
|
1903
|
-
{el.materialType === "viewport" ? (
|
|
1904
|
-
<label className="block space-y-1.5">
|
|
1905
|
-
<div>{t("panel.config.viewportOverflow")}</div>
|
|
1906
|
-
<Select
|
|
1907
|
-
value={normalizeViewportOverflow(el.viewportOverflow)}
|
|
1908
|
-
onValueChange={(value) =>
|
|
1909
|
-
updateElement(el.id, {
|
|
1910
|
-
viewportOverflow: value as ViewportOverflowMode,
|
|
1911
|
-
})
|
|
1912
|
-
}
|
|
1913
|
-
>
|
|
1914
|
-
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1915
|
-
<SelectContent>
|
|
1916
|
-
{VIEWPORT_OVERFLOW_MODES.map((mode) => (
|
|
1917
|
-
<SelectItem key={mode} value={mode}>
|
|
1918
|
-
{mode === "scroll-x"
|
|
1919
|
-
? t("panel.config.viewportOverflowScrollX")
|
|
1920
|
-
: mode === "scroll-y"
|
|
1921
|
-
? t("panel.config.viewportOverflowScrollY")
|
|
1922
|
-
: mode === "scroll"
|
|
1923
|
-
? t("panel.config.viewportOverflowScroll")
|
|
1924
|
-
: t("panel.config.viewportOverflowClip")}
|
|
1925
|
-
</SelectItem>
|
|
1926
|
-
))}
|
|
1927
|
-
</SelectContent>
|
|
1928
|
-
</Select>
|
|
1929
|
-
</label>
|
|
1930
|
-
) : null}
|
|
1931
|
-
</div>
|
|
1932
|
-
</fieldset>
|
|
1933
|
-
</CardContent>
|
|
1934
402
|
) : null}
|
|
1935
|
-
</
|
|
1936
|
-
)
|
|
1937
|
-
|
|
1938
|
-
effectiveSelectedElements.every((el) => {
|
|
1939
|
-
const text = `${el.name ?? ""} ${el.id} ${el.materialType ?? ""} zIndex style layer`
|
|
1940
|
-
.toLowerCase();
|
|
1941
|
-
return !text.includes(normalizedSearch);
|
|
1942
|
-
}) ? (
|
|
1943
|
-
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
1944
|
-
{t("panel.config.noEditableMatch")}
|
|
1945
|
-
</div>
|
|
1946
|
-
) : null}
|
|
403
|
+
</div>
|
|
404
|
+
) : null}
|
|
405
|
+
</div>
|
|
1947
406
|
</div>
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
<
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
<
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
407
|
+
{isMultiSelectMode ? (
|
|
408
|
+
<PanelConfigMultiSelect
|
|
409
|
+
elements={effectiveSelectedElements}
|
|
410
|
+
helpers={helpers}
|
|
411
|
+
updateElement={updateElement}
|
|
412
|
+
layers={layers}
|
|
413
|
+
setReferenceCopyMode={setReferenceCopyMode}
|
|
414
|
+
onExcludeSelectedNode={onExcludeSelectedNode}
|
|
415
|
+
onAdjustNodeZOrder={onAdjustNodeZOrder}
|
|
416
|
+
/>
|
|
417
|
+
) : null}
|
|
418
|
+
{!isMultiSelectMode && !selectedElement ? (
|
|
419
|
+
<Empty className="py-7">
|
|
420
|
+
<EmptyIcon>
|
|
421
|
+
<svg
|
|
422
|
+
viewBox="0 0 24 24"
|
|
423
|
+
className="h-4 w-4"
|
|
424
|
+
fill="none"
|
|
425
|
+
stroke="currentColor"
|
|
426
|
+
strokeWidth="1.8"
|
|
427
|
+
aria-hidden="true"
|
|
428
|
+
>
|
|
429
|
+
<rect x="4" y="4" width="16" height="16" rx="2.5" />
|
|
430
|
+
<path d="M8 9h8M8 12h8M8 15h5" />
|
|
431
|
+
</svg>
|
|
432
|
+
</EmptyIcon>
|
|
433
|
+
<EmptyTitle>{t("panel.config.emptyNoNodeTitle")}</EmptyTitle>
|
|
434
|
+
<EmptyDescription>{t("panel.config.emptyNoNodeDesc")}</EmptyDescription>
|
|
435
|
+
</Empty>
|
|
436
|
+
) : !isMultiSelectMode && selectedElement ? (
|
|
437
|
+
<div className="space-y-3">
|
|
438
|
+
{!isNodeEditable ? (
|
|
439
|
+
<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">
|
|
440
|
+
{readonlyReason}
|
|
441
|
+
</div>
|
|
442
|
+
) : null}
|
|
443
|
+
<fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
|
|
444
|
+
<div className="space-y-3.5 text-xs">
|
|
445
|
+
<PanelConfigNodeInfo
|
|
446
|
+
element={selectedElement}
|
|
447
|
+
helpers={helpers}
|
|
448
|
+
updateElement={updateElement}
|
|
449
|
+
nodeZOrderLabel={nodeZOrderLabel}
|
|
450
|
+
onAdjustNodeZOrder={onAdjustNodeZOrder}
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
</fieldset>
|
|
454
|
+
<div className="rounded-lg border border-border/60 bg-card/80 px-2.5 py-2 text-xs">
|
|
455
|
+
<label className="flex items-center gap-2">
|
|
456
|
+
<Checkbox
|
|
457
|
+
checked={selectedElement.locked === true}
|
|
458
|
+
disabled={!canToggleNodeLock}
|
|
459
|
+
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"
|
|
460
|
+
onCheckedChange={(checked) =>
|
|
1985
461
|
updateElement(selectedElement.id, {
|
|
1986
|
-
|
|
462
|
+
locked: checked === true,
|
|
1987
463
|
})
|
|
1988
464
|
}
|
|
1989
|
-
placeholder={t("panel.config.nodeNamePlaceholder")}
|
|
1990
|
-
className="h-7"
|
|
1991
465
|
/>
|
|
466
|
+
<span>{t("panel.config.lockNode")}</span>
|
|
1992
467
|
</label>
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
x: Number(e.target.value) || 0,
|
|
2002
|
-
})
|
|
2003
|
-
}
|
|
2004
|
-
className="h-7"
|
|
468
|
+
</div>
|
|
469
|
+
<fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
|
|
470
|
+
<div className="space-y-3.5 text-xs">
|
|
471
|
+
{isChartElement ? (
|
|
472
|
+
<PanelConfigChartSection
|
|
473
|
+
element={selectedElement}
|
|
474
|
+
helpers={helpers}
|
|
475
|
+
updateElement={updateElement}
|
|
2005
476
|
/>
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
updateElement
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
477
|
+
) : materialType === "table" ? (
|
|
478
|
+
helpers.renderSection(
|
|
479
|
+
"tableConfig",
|
|
480
|
+
t("panel.config.sectionTable"),
|
|
481
|
+
<PanelConfigTableSection
|
|
482
|
+
element={selectedElement}
|
|
483
|
+
disabled={!isNodeEditable}
|
|
484
|
+
updateElement={updateElement}
|
|
485
|
+
blueprintNodeOptions={blueprintNodeOptions}
|
|
486
|
+
/>,
|
|
487
|
+
true,
|
|
488
|
+
[
|
|
489
|
+
t("panel.material.table"),
|
|
490
|
+
"table",
|
|
491
|
+
t("panel.config.tableSource"),
|
|
492
|
+
t("panel.config.tableColumns"),
|
|
493
|
+
t("panel.config.tableRowsText"),
|
|
494
|
+
]
|
|
495
|
+
)
|
|
496
|
+
) : materialType === "text" ? (
|
|
497
|
+
<PanelConfigTextSection
|
|
498
|
+
element={selectedElement}
|
|
499
|
+
helpers={helpers}
|
|
500
|
+
updateElement={updateElement}
|
|
2018
501
|
/>
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
<Input
|
|
2025
|
-
type="number"
|
|
2026
|
-
min={1}
|
|
2027
|
-
value={selectedElement?.width ?? 1}
|
|
2028
|
-
onChange={(e) => {
|
|
2029
|
-
if (!selectedElement) return;
|
|
2030
|
-
updateElement(selectedElement.id, {
|
|
2031
|
-
width: Math.max(1, Number(e.target.value) || 1),
|
|
2032
|
-
});
|
|
2033
|
-
}}
|
|
2034
|
-
className="h-7"
|
|
502
|
+
) : materialType === "audio" ? (
|
|
503
|
+
<PanelConfigAudioSection
|
|
504
|
+
element={selectedElement}
|
|
505
|
+
helpers={helpers}
|
|
506
|
+
updateElement={updateElement}
|
|
2035
507
|
/>
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
min={1}
|
|
2042
|
-
value={selectedElement.height}
|
|
2043
|
-
onChange={(e) =>
|
|
2044
|
-
updateElement(selectedElement.id, {
|
|
2045
|
-
height: Math.max(1, Number(e.target.value) || 1),
|
|
2046
|
-
})
|
|
2047
|
-
}
|
|
2048
|
-
className="h-7"
|
|
508
|
+
) : materialType === "video" ? (
|
|
509
|
+
<PanelConfigVideoSection
|
|
510
|
+
element={selectedElement}
|
|
511
|
+
helpers={helpers}
|
|
512
|
+
updateElement={updateElement}
|
|
2049
513
|
/>
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
<
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
onChange={(e) =>
|
|
2062
|
-
updateElement(selectedElement.id, {
|
|
2063
|
-
rotate: Number(e.target.value) || 0,
|
|
2064
|
-
})
|
|
2065
|
-
}
|
|
2066
|
-
className="h-7"
|
|
514
|
+
) : null}
|
|
515
|
+
<PanelConfigGridChildSpan
|
|
516
|
+
element={selectedElement}
|
|
517
|
+
helpers={helpers}
|
|
518
|
+
updateElement={updateElement}
|
|
519
|
+
/>
|
|
520
|
+
{materialType === "geometry" ? (
|
|
521
|
+
<PanelConfigGeometrySection
|
|
522
|
+
element={selectedElement}
|
|
523
|
+
helpers={helpers}
|
|
524
|
+
updateElement={updateElement}
|
|
2067
525
|
/>
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
{
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
{
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
{
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
)}
|
|
2112
|
-
</>,
|
|
2113
|
-
true,
|
|
2114
|
-
[t("panel.config.name"), "id", t("panel.config.type"), t("panel.config.rotate"), t("panel.config.nodeZOrder"), t("panel.layers.lockShort"), "locked", "name"]
|
|
2115
|
-
)}
|
|
2116
|
-
</div>
|
|
2117
|
-
</fieldset>
|
|
2118
|
-
<div className="rounded-lg border border-border/60 bg-card/80 px-2.5 py-2 text-xs">
|
|
2119
|
-
<label className="flex items-center gap-2">
|
|
2120
|
-
<Checkbox
|
|
2121
|
-
checked={selectedElement.locked === true}
|
|
2122
|
-
disabled={!canToggleNodeLock}
|
|
2123
|
-
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"
|
|
2124
|
-
onCheckedChange={(checked) =>
|
|
2125
|
-
updateElement(selectedElement.id, {
|
|
2126
|
-
locked: checked === true,
|
|
2127
|
-
})
|
|
2128
|
-
}
|
|
2129
|
-
/>
|
|
2130
|
-
<span>{t("panel.config.lockNode")}</span>
|
|
2131
|
-
</label>
|
|
2132
|
-
</div>
|
|
2133
|
-
<fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
|
|
2134
|
-
<div className="space-y-3.5 text-xs">
|
|
2135
|
-
|
|
2136
|
-
{isChartElement ? (
|
|
2137
|
-
<>
|
|
2138
|
-
{renderSection(
|
|
2139
|
-
"chartBasic",
|
|
2140
|
-
t("panel.config.sectionChartBasic"),
|
|
2141
|
-
<>
|
|
2142
|
-
{renderFieldGroup(
|
|
2143
|
-
t("panel.config.groupBasicDisplay"),
|
|
2144
|
-
<>
|
|
2145
|
-
<label className="block space-y-1.5" data-config-field="chart.title">
|
|
2146
|
-
<div>{t("panel.config.title")}</div>
|
|
2147
|
-
<Input
|
|
2148
|
-
value={selectedElement.chart?.title ?? ""}
|
|
2149
|
-
onChange={(e) => updateSelectedChart({ title: e.target.value })}
|
|
2150
|
-
className="h-7"
|
|
2151
|
-
/>
|
|
2152
|
-
</label>
|
|
2153
|
-
|
|
2154
|
-
{renderColorField(
|
|
2155
|
-
t("panel.config.primaryColor"),
|
|
2156
|
-
selectedElement.chart?.color ?? "#3b82f6",
|
|
2157
|
-
(next) => updateSelectedChart({ color: next || "#3b82f6" })
|
|
2158
|
-
)}
|
|
2159
|
-
</>
|
|
2160
|
-
)}
|
|
2161
|
-
{renderFieldGroup(
|
|
2162
|
-
t("panel.config.groupData"),
|
|
2163
|
-
<>
|
|
2164
|
-
<label className="block space-y-1">
|
|
2165
|
-
<div>{t("panel.config.labelsCsv")}</div>
|
|
2166
|
-
<Input
|
|
2167
|
-
value={getChartLabelsDisplayText(selectedElement.chart)}
|
|
2168
|
-
onChange={(e) =>
|
|
2169
|
-
updateSelectedChart({
|
|
2170
|
-
labelsText: e.target.value,
|
|
2171
|
-
})
|
|
2172
|
-
}
|
|
2173
|
-
className="h-7"
|
|
2174
|
-
/>
|
|
2175
|
-
</label>
|
|
2176
|
-
|
|
2177
|
-
<label className="block space-y-1">
|
|
2178
|
-
<div>{t("panel.config.valuesCsv")}</div>
|
|
2179
|
-
<Input
|
|
2180
|
-
value={getChartValuesDisplayText(selectedElement.chart)}
|
|
2181
|
-
onChange={(e) =>
|
|
2182
|
-
updateSelectedChart({
|
|
2183
|
-
valuesText: e.target.value,
|
|
2184
|
-
})
|
|
2185
|
-
}
|
|
2186
|
-
className="h-7"
|
|
2187
|
-
/>
|
|
2188
|
-
</label>
|
|
2189
|
-
</>
|
|
2190
|
-
)}
|
|
2191
|
-
|
|
2192
|
-
{selectedChartType === "bar" ||
|
|
2193
|
-
selectedChartType === "line" ||
|
|
2194
|
-
selectedChartType === "area" ||
|
|
2195
|
-
selectedChartType === "pie" ? (
|
|
2196
|
-
renderFieldGroup(
|
|
2197
|
-
t("panel.config.groupSeries"),
|
|
2198
|
-
<>
|
|
2199
|
-
{selectedChartType === "bar" ? (
|
|
2200
|
-
<label className="block space-y-1">
|
|
2201
|
-
<div>{t("panel.config.barWidthPx")}</div>
|
|
2202
|
-
<Input
|
|
2203
|
-
type="number"
|
|
2204
|
-
min={1}
|
|
2205
|
-
placeholder={t("panel.config.barWidthAuto")}
|
|
2206
|
-
value={selectedElement.chart?.barWidth ?? ""}
|
|
2207
|
-
onChange={(e) => {
|
|
2208
|
-
const raw = e.target.value.trim();
|
|
2209
|
-
updateSelectedChart({
|
|
2210
|
-
barWidth: raw
|
|
2211
|
-
? Math.max(1, Number(raw) || 1)
|
|
2212
|
-
: undefined,
|
|
2213
|
-
});
|
|
2214
|
-
}}
|
|
2215
|
-
className="h-7"
|
|
2216
|
-
/>
|
|
2217
|
-
</label>
|
|
2218
|
-
) : null}
|
|
2219
|
-
|
|
2220
|
-
{selectedChartType === "line" || selectedChartType === "area" ? (
|
|
2221
|
-
<label className="flex items-center gap-2">
|
|
2222
|
-
<Checkbox
|
|
2223
|
-
checked={selectedElement.chart?.smooth ?? true}
|
|
2224
|
-
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"
|
|
2225
|
-
onCheckedChange={(checked) =>
|
|
2226
|
-
updateSelectedChart({ smooth: checked === true })
|
|
2227
|
-
}
|
|
2228
|
-
/>
|
|
2229
|
-
<span>{t("panel.config.smooth")}</span>
|
|
2230
|
-
</label>
|
|
2231
|
-
) : null}
|
|
2232
|
-
|
|
2233
|
-
{selectedChartType === "pie" ? (
|
|
2234
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2235
|
-
<label className="block space-y-1">
|
|
2236
|
-
<div>{t("panel.config.pieInnerRadiusPct")}</div>
|
|
2237
|
-
<Input
|
|
2238
|
-
type="number"
|
|
2239
|
-
min={0}
|
|
2240
|
-
max={99}
|
|
2241
|
-
value={selectedElement.chart?.pieInnerRadius ?? 30}
|
|
2242
|
-
onChange={(e) =>
|
|
2243
|
-
updateSelectedChart({
|
|
2244
|
-
pieInnerRadius: Math.max(
|
|
2245
|
-
0,
|
|
2246
|
-
Math.min(99, Number(e.target.value) || 0)
|
|
2247
|
-
),
|
|
2248
|
-
})
|
|
2249
|
-
}
|
|
2250
|
-
className="h-7"
|
|
2251
|
-
/>
|
|
2252
|
-
</label>
|
|
2253
|
-
<label className="block space-y-1">
|
|
2254
|
-
<div>{t("panel.config.pieOuterRadiusPct")}</div>
|
|
2255
|
-
<Input
|
|
2256
|
-
type="number"
|
|
2257
|
-
min={1}
|
|
2258
|
-
max={100}
|
|
2259
|
-
value={selectedElement.chart?.pieOuterRadius ?? 65}
|
|
2260
|
-
onChange={(e) =>
|
|
2261
|
-
updateSelectedChart({
|
|
2262
|
-
pieOuterRadius: Math.max(
|
|
2263
|
-
1,
|
|
2264
|
-
Math.min(100, Number(e.target.value) || 1)
|
|
2265
|
-
),
|
|
2266
|
-
})
|
|
2267
|
-
}
|
|
2268
|
-
className="h-7"
|
|
2269
|
-
/>
|
|
2270
|
-
</label>
|
|
2271
|
-
</div>
|
|
2272
|
-
) : null}
|
|
2273
|
-
</>
|
|
2274
|
-
)
|
|
2275
|
-
) : null}
|
|
2276
|
-
{renderFieldGroup(
|
|
2277
|
-
t("panel.config.groupChartDisplayMore"),
|
|
2278
|
-
<>
|
|
2279
|
-
<label className="flex items-center gap-2">
|
|
2280
|
-
<Checkbox
|
|
2281
|
-
checked={selectedElement.chart?.colorMode === "gradient"}
|
|
2282
|
-
onCheckedChange={(checked) =>
|
|
2283
|
-
updateSelectedChart({ colorMode: checked ? "gradient" : "solid" })
|
|
2284
|
-
}
|
|
2285
|
-
/>
|
|
2286
|
-
<span>{t("panel.config.usePrimaryGradient")}</span>
|
|
2287
|
-
</label>
|
|
2288
|
-
{selectedElement.chart?.colorMode === "gradient" ? (
|
|
2289
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2290
|
-
{renderColorField(
|
|
2291
|
-
t("panel.config.gradientFrom"),
|
|
2292
|
-
selectedElement.chart?.gradientFrom ?? selectedElement.chart?.color ?? "#3b82f6",
|
|
2293
|
-
(next) => updateSelectedChart({ gradientFrom: next || "#3b82f6" })
|
|
2294
|
-
)}
|
|
2295
|
-
{renderColorField(
|
|
2296
|
-
t("panel.config.gradientTo"),
|
|
2297
|
-
selectedElement.chart?.gradientTo ?? "#22d3ee",
|
|
2298
|
-
(next) => updateSelectedChart({ gradientTo: next || "#22d3ee" })
|
|
2299
|
-
)}
|
|
2300
|
-
<label className="block space-y-1 col-span-2">
|
|
2301
|
-
<div>{t("panel.config.gradientDirection")}</div>
|
|
2302
|
-
<Select
|
|
2303
|
-
value={selectedElement.chart?.gradientDirection ?? "to-right"}
|
|
2304
|
-
onValueChange={(value) =>
|
|
2305
|
-
updateSelectedChart({
|
|
2306
|
-
gradientDirection: value as
|
|
2307
|
-
| "to-right"
|
|
2308
|
-
| "to-bottom"
|
|
2309
|
-
| "to-bottom-right"
|
|
2310
|
-
| "to-top-right",
|
|
2311
|
-
})
|
|
2312
|
-
}
|
|
2313
|
-
>
|
|
2314
|
-
<SelectTrigger className="h-7">
|
|
2315
|
-
<SelectValue placeholder={t("panel.config.selectGradientDirection")} />
|
|
2316
|
-
</SelectTrigger>
|
|
2317
|
-
<SelectContent>
|
|
2318
|
-
<SelectItem value="to-right">{t("panel.config.dirToRight")}</SelectItem>
|
|
2319
|
-
<SelectItem value="to-bottom">{t("panel.config.dirToBottom")}</SelectItem>
|
|
2320
|
-
<SelectItem value="to-bottom-right">{t("panel.config.dirToBottomRight")}</SelectItem>
|
|
2321
|
-
<SelectItem value="to-top-right">{t("panel.config.dirToTopRight")}</SelectItem>
|
|
2322
|
-
</SelectContent>
|
|
2323
|
-
</Select>
|
|
2324
|
-
</label>
|
|
2325
|
-
<div className="col-span-2 space-y-1">
|
|
2326
|
-
<div className="text-[11px] text-muted-foreground">{t("panel.config.gradientPreview")}</div>
|
|
2327
|
-
<div
|
|
2328
|
-
className="h-6 rounded border border-border/60"
|
|
2329
|
-
style={{
|
|
2330
|
-
backgroundImage: `linear-gradient(${
|
|
2331
|
-
(selectedElement.chart?.gradientDirection ?? "to-right") === "to-bottom"
|
|
2332
|
-
? "to bottom"
|
|
2333
|
-
: (selectedElement.chart?.gradientDirection ?? "to-right") === "to-bottom-right"
|
|
2334
|
-
? "to bottom right"
|
|
2335
|
-
: (selectedElement.chart?.gradientDirection ?? "to-right") === "to-top-right"
|
|
2336
|
-
? "to top right"
|
|
2337
|
-
: "to right"
|
|
2338
|
-
}, ${selectedElement.chart?.gradientFrom ?? selectedElement.chart?.color ?? "#3b82f6"}, ${selectedElement.chart?.gradientTo ?? "#22d3ee"})`,
|
|
2339
|
-
}}
|
|
2340
|
-
/>
|
|
2341
|
-
</div>
|
|
2342
|
-
</div>
|
|
2343
|
-
) : null}
|
|
2344
|
-
<label className="block space-y-1.5">
|
|
2345
|
-
<div>{t("panel.config.renderMode")}</div>
|
|
2346
|
-
<Select
|
|
2347
|
-
value={selectedElement.chart?.renderMode ?? "canvas"}
|
|
2348
|
-
onValueChange={(value) =>
|
|
2349
|
-
updateSelectedChart({ renderMode: value as "canvas" | "svg" })
|
|
2350
|
-
}
|
|
2351
|
-
>
|
|
2352
|
-
<SelectTrigger className="h-7">
|
|
2353
|
-
<SelectValue placeholder={t("panel.config.selectRenderMode")} />
|
|
2354
|
-
</SelectTrigger>
|
|
2355
|
-
<SelectContent>
|
|
2356
|
-
<SelectItem value="canvas">Canvas</SelectItem>
|
|
2357
|
-
<SelectItem value="svg">SVG</SelectItem>
|
|
2358
|
-
</SelectContent>
|
|
2359
|
-
</Select>
|
|
2360
|
-
</label>
|
|
2361
|
-
</>,
|
|
2362
|
-
undefined,
|
|
2363
|
-
{ groupKey: "chartDisplayMore", defaultOpen: false }
|
|
2364
|
-
)}
|
|
2365
|
-
{renderFieldGroup(
|
|
2366
|
-
t("panel.config.groupTooltip"),
|
|
2367
|
-
<>
|
|
2368
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2369
|
-
<label className="flex items-center gap-2">
|
|
2370
|
-
<Checkbox
|
|
2371
|
-
checked={selectedElement.chart?.tooltipShow ?? true}
|
|
2372
|
-
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"
|
|
2373
|
-
onCheckedChange={(checked) =>
|
|
2374
|
-
updateSelectedChart({ tooltipShow: checked === true })
|
|
2375
|
-
}
|
|
2376
|
-
/>
|
|
2377
|
-
<span>{t("panel.config.showTooltip")}</span>
|
|
2378
|
-
</label>
|
|
2379
|
-
<label className="block space-y-1">
|
|
2380
|
-
<div>{t("panel.config.tooltipTrigger")}</div>
|
|
2381
|
-
<Select
|
|
2382
|
-
value={selectedElement.chart?.tooltipTrigger ?? "axis"}
|
|
2383
|
-
onValueChange={(value) =>
|
|
2384
|
-
updateSelectedChart({ tooltipTrigger: value as "axis" | "item" })
|
|
2385
|
-
}
|
|
2386
|
-
>
|
|
2387
|
-
<SelectTrigger className="h-7">
|
|
2388
|
-
<SelectValue placeholder={t("panel.config.selectTooltipTrigger")} />
|
|
2389
|
-
</SelectTrigger>
|
|
2390
|
-
<SelectContent>
|
|
2391
|
-
<SelectItem value="axis">axis</SelectItem>
|
|
2392
|
-
<SelectItem value="item">item</SelectItem>
|
|
2393
|
-
</SelectContent>
|
|
2394
|
-
</Select>
|
|
2395
|
-
</label>
|
|
2396
|
-
</div>
|
|
2397
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2398
|
-
{renderColorField(
|
|
2399
|
-
t("panel.config.tooltipBgColor"),
|
|
2400
|
-
selectedElement.chart?.tooltipBackgroundColor ?? "#0f172a",
|
|
2401
|
-
(next) => updateSelectedChart({ tooltipBackgroundColor: next || "#0f172a" })
|
|
2402
|
-
)}
|
|
2403
|
-
{renderColorField(
|
|
2404
|
-
t("panel.config.tooltipTextColor"),
|
|
2405
|
-
selectedElement.chart?.tooltipTextColor ?? "#f8fafc",
|
|
2406
|
-
(next) => updateSelectedChart({ tooltipTextColor: next || "#f8fafc" })
|
|
2407
|
-
)}
|
|
2408
|
-
</div>
|
|
2409
|
-
<label className="block space-y-1">
|
|
2410
|
-
{renderFormatterLabel(t("panel.config.tooltipFormatter"))}
|
|
2411
|
-
<Input
|
|
2412
|
-
value={selectedElement.chart?.tooltipFormatter ?? ""}
|
|
2413
|
-
onChange={(e) =>
|
|
2414
|
-
updateSelectedChart({ tooltipFormatter: e.target.value || undefined })
|
|
2415
|
-
}
|
|
2416
|
-
placeholder={t("panel.config.tooltipFormatterPlaceholder")}
|
|
2417
|
-
className="h-7"
|
|
2418
|
-
/>
|
|
2419
|
-
</label>
|
|
2420
|
-
</>,
|
|
2421
|
-
undefined,
|
|
2422
|
-
{ groupKey: "chartTooltip", defaultOpen: false }
|
|
2423
|
-
)}
|
|
2424
|
-
{selectedChartType === "bar" ||
|
|
2425
|
-
selectedChartType === "line" ||
|
|
2426
|
-
selectedChartType === "area" ||
|
|
2427
|
-
selectedChartType === "scatter" ? (
|
|
2428
|
-
renderFieldGroup(
|
|
2429
|
-
t("panel.config.groupAxes"),
|
|
2430
|
-
<>
|
|
2431
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2432
|
-
<label className="block">
|
|
2433
|
-
<div className="mb-1">{t("panel.config.xAxisName")}</div>
|
|
2434
|
-
<Input
|
|
2435
|
-
value={selectedElement.chart?.xAxisName ?? ""}
|
|
2436
|
-
onChange={(e) => updateSelectedChart({ xAxisName: e.target.value })}
|
|
2437
|
-
placeholder={t("panel.config.xAxisNamePlaceholder")}
|
|
2438
|
-
className="h-7"
|
|
2439
|
-
/>
|
|
2440
|
-
</label>
|
|
2441
|
-
<label className="block">
|
|
2442
|
-
<div className="mb-1">{t("panel.config.yAxisName")}</div>
|
|
2443
|
-
<Input
|
|
2444
|
-
value={selectedElement.chart?.yAxisName ?? ""}
|
|
2445
|
-
onChange={(e) => updateSelectedChart({ yAxisName: e.target.value })}
|
|
2446
|
-
placeholder={t("panel.config.yAxisNamePlaceholder")}
|
|
2447
|
-
className="h-7"
|
|
2448
|
-
/>
|
|
2449
|
-
</label>
|
|
2450
|
-
</div>
|
|
2451
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2452
|
-
<label className="flex items-center gap-2">
|
|
2453
|
-
<Checkbox
|
|
2454
|
-
checked={selectedElement.chart?.xAxisTickShow ?? true}
|
|
2455
|
-
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"
|
|
2456
|
-
onCheckedChange={(checked) =>
|
|
2457
|
-
updateSelectedChart({ xAxisTickShow: checked === true })
|
|
2458
|
-
}
|
|
2459
|
-
/>
|
|
2460
|
-
<span>{t("panel.config.xAxisTick")}</span>
|
|
2461
|
-
</label>
|
|
2462
|
-
<label className="flex items-center gap-2">
|
|
2463
|
-
<Checkbox
|
|
2464
|
-
checked={selectedElement.chart?.yAxisTickShow ?? true}
|
|
2465
|
-
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"
|
|
2466
|
-
onCheckedChange={(checked) =>
|
|
2467
|
-
updateSelectedChart({ yAxisTickShow: checked === true })
|
|
2468
|
-
}
|
|
2469
|
-
/>
|
|
2470
|
-
<span>{t("panel.config.yAxisTick")}</span>
|
|
2471
|
-
</label>
|
|
2472
|
-
</div>
|
|
2473
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2474
|
-
{renderColorField(
|
|
2475
|
-
t("panel.config.xAxisTickColor"),
|
|
2476
|
-
selectedElement.chart?.xAxisTickColor ?? "#94a3b8",
|
|
2477
|
-
(next) => updateSelectedChart({ xAxisTickColor: next || "#94a3b8" })
|
|
2478
|
-
)}
|
|
2479
|
-
{renderColorField(
|
|
2480
|
-
t("panel.config.yAxisTickColor"),
|
|
2481
|
-
selectedElement.chart?.yAxisTickColor ?? "#94a3b8",
|
|
2482
|
-
(next) => updateSelectedChart({ yAxisTickColor: next || "#94a3b8" })
|
|
2483
|
-
)}
|
|
2484
|
-
</div>
|
|
2485
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2486
|
-
<label className="flex items-center gap-2">
|
|
2487
|
-
<Checkbox
|
|
2488
|
-
checked={selectedElement.chart?.xAxisSplitLineShow ?? false}
|
|
2489
|
-
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"
|
|
2490
|
-
onCheckedChange={(checked) =>
|
|
2491
|
-
updateSelectedChart({ xAxisSplitLineShow: checked === true })
|
|
2492
|
-
}
|
|
2493
|
-
/>
|
|
2494
|
-
<span>{t("panel.config.xAxisSplitLine")}</span>
|
|
2495
|
-
</label>
|
|
2496
|
-
<label className="flex items-center gap-2">
|
|
2497
|
-
<Checkbox
|
|
2498
|
-
checked={selectedElement.chart?.yAxisSplitLineShow ?? true}
|
|
2499
|
-
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"
|
|
2500
|
-
onCheckedChange={(checked) =>
|
|
2501
|
-
updateSelectedChart({ yAxisSplitLineShow: checked === true })
|
|
2502
|
-
}
|
|
2503
|
-
/>
|
|
2504
|
-
<span>{t("panel.config.yAxisSplitLine")}</span>
|
|
2505
|
-
</label>
|
|
2506
|
-
</div>
|
|
2507
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2508
|
-
{renderColorField(
|
|
2509
|
-
t("panel.config.xAxisSplitLineColor"),
|
|
2510
|
-
selectedElement.chart?.xAxisSplitLineColor ?? "#e2e8f0",
|
|
2511
|
-
(next) => updateSelectedChart({ xAxisSplitLineColor: next || "#e2e8f0" })
|
|
2512
|
-
)}
|
|
2513
|
-
{renderColorField(
|
|
2514
|
-
t("panel.config.yAxisSplitLineColor"),
|
|
2515
|
-
selectedElement.chart?.yAxisSplitLineColor ?? "#e2e8f0",
|
|
2516
|
-
(next) => updateSelectedChart({ yAxisSplitLineColor: next || "#e2e8f0" })
|
|
2517
|
-
)}
|
|
2518
|
-
</div>
|
|
2519
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2520
|
-
{renderColorField(
|
|
2521
|
-
t("panel.config.xAxisLabelColor"),
|
|
2522
|
-
selectedElement.chart?.xAxisLabelColor ?? "#64748b",
|
|
2523
|
-
(next) => updateSelectedChart({ xAxisLabelColor: next || "#64748b" })
|
|
2524
|
-
)}
|
|
2525
|
-
{renderColorField(
|
|
2526
|
-
t("panel.config.yAxisLabelColor"),
|
|
2527
|
-
selectedElement.chart?.yAxisLabelColor ?? "#64748b",
|
|
2528
|
-
(next) => updateSelectedChart({ yAxisLabelColor: next || "#64748b" })
|
|
2529
|
-
)}
|
|
2530
|
-
</div>
|
|
2531
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2532
|
-
<label className="block">
|
|
2533
|
-
<div className="mb-1">{t("panel.config.xAxisLabelFontSize")}</div>
|
|
2534
|
-
<Input
|
|
2535
|
-
type="number"
|
|
2536
|
-
min={8}
|
|
2537
|
-
max={48}
|
|
2538
|
-
value={selectedElement.chart?.xAxisLabelFontSize ?? 10}
|
|
2539
|
-
onChange={(e) =>
|
|
2540
|
-
updateSelectedChart({
|
|
2541
|
-
xAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
|
|
2542
|
-
})
|
|
2543
|
-
}
|
|
2544
|
-
className="h-7"
|
|
2545
|
-
/>
|
|
2546
|
-
</label>
|
|
2547
|
-
<label className="block">
|
|
2548
|
-
<div className="mb-1">{t("panel.config.yAxisLabelFontSize")}</div>
|
|
2549
|
-
<Input
|
|
2550
|
-
type="number"
|
|
2551
|
-
min={8}
|
|
2552
|
-
max={48}
|
|
2553
|
-
value={selectedElement.chart?.yAxisLabelFontSize ?? 10}
|
|
2554
|
-
onChange={(e) =>
|
|
2555
|
-
updateSelectedChart({
|
|
2556
|
-
yAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
|
|
2557
|
-
})
|
|
2558
|
-
}
|
|
2559
|
-
className="h-7"
|
|
2560
|
-
/>
|
|
2561
|
-
</label>
|
|
2562
|
-
</div>
|
|
2563
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2564
|
-
<label className="flex items-center gap-2">
|
|
2565
|
-
<Checkbox
|
|
2566
|
-
checked={selectedElement.chart?.xAxisLabelAutoEllipsis ?? false}
|
|
2567
|
-
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"
|
|
2568
|
-
onCheckedChange={(checked) =>
|
|
2569
|
-
updateSelectedChart({ xAxisLabelAutoEllipsis: checked === true })
|
|
2570
|
-
}
|
|
2571
|
-
/>
|
|
2572
|
-
<span>{t("panel.config.xAxisLabelAutoEllipsis")}</span>
|
|
2573
|
-
</label>
|
|
2574
|
-
<label className="flex items-center gap-2">
|
|
2575
|
-
<Checkbox
|
|
2576
|
-
checked={selectedElement.chart?.yAxisLabelAutoEllipsis ?? false}
|
|
2577
|
-
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"
|
|
2578
|
-
onCheckedChange={(checked) =>
|
|
2579
|
-
updateSelectedChart({ yAxisLabelAutoEllipsis: checked === true })
|
|
2580
|
-
}
|
|
2581
|
-
/>
|
|
2582
|
-
<span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
|
|
2583
|
-
</label>
|
|
2584
|
-
</div>
|
|
2585
|
-
</>,
|
|
2586
|
-
undefined,
|
|
2587
|
-
{ groupKey: "chartAxes", defaultOpen: false }
|
|
2588
|
-
)
|
|
2589
|
-
) : null}
|
|
2590
|
-
|
|
2591
|
-
</>,
|
|
2592
|
-
true,
|
|
2593
|
-
[
|
|
2594
|
-
t("panel.config.searchKwChart"),
|
|
2595
|
-
"title",
|
|
2596
|
-
"tooltip",
|
|
2597
|
-
t("panel.config.searchKwXAxis"),
|
|
2598
|
-
t("panel.config.searchKwYAxis"),
|
|
2599
|
-
"axis",
|
|
2600
|
-
"label",
|
|
2601
|
-
t("panel.config.searchKwTick"),
|
|
2602
|
-
t("panel.config.searchKwSplit"),
|
|
2603
|
-
"render",
|
|
2604
|
-
"svg",
|
|
2605
|
-
"canvas",
|
|
2606
|
-
]
|
|
2607
|
-
)}
|
|
2608
|
-
|
|
2609
|
-
{renderSection(
|
|
2610
|
-
"chartAdvanced",
|
|
2611
|
-
t("panel.config.sectionChartAdvanced"),
|
|
2612
|
-
<>
|
|
2613
|
-
{renderFieldGroup(
|
|
2614
|
-
t("panel.config.groupCommonForm"),
|
|
2615
|
-
<div className="grid grid-cols-1 gap-2">
|
|
2616
|
-
<div className="rounded-lg border border-border/60 bg-background/70 p-3">
|
|
2617
|
-
<Collapsible
|
|
2618
|
-
open={isSectionExpanded("chartAdvancedLayout", false)}
|
|
2619
|
-
onOpenChange={(open) => setSectionExpanded("chartAdvancedLayout", open)}
|
|
2620
|
-
>
|
|
2621
|
-
<div className="mb-2 flex items-center gap-1.5">
|
|
2622
|
-
<CollapsibleTrigger asChild>
|
|
2623
|
-
<button
|
|
2624
|
-
type="button"
|
|
2625
|
-
className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
|
|
2626
|
-
aria-label={isSectionExpanded("chartAdvancedLayout", false) ? t("panel.config.collapseLayoutCoord") : t("panel.config.expandLayoutCoord")}
|
|
2627
|
-
>
|
|
2628
|
-
{isSectionExpanded("chartAdvancedLayout", false) ? "▾" : "▸"}
|
|
2629
|
-
</button>
|
|
2630
|
-
</CollapsibleTrigger>
|
|
2631
|
-
<div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupLayoutAndCoord")}</div>
|
|
2632
|
-
</div>
|
|
2633
|
-
<CollapsibleContent>
|
|
2634
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2635
|
-
<label className="block space-y-1.5">
|
|
2636
|
-
<div className={optionLabelTextClass}>{t("panel.config.gridLeftForm")}</div>
|
|
2637
|
-
<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 } })} />
|
|
2638
|
-
</label>
|
|
2639
|
-
<label className="block space-y-1.5">
|
|
2640
|
-
<div className={optionLabelTextClass}>{t("panel.config.gridRightForm")}</div>
|
|
2641
|
-
<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 } })} />
|
|
2642
|
-
</label>
|
|
2643
|
-
<label className="block space-y-1.5">
|
|
2644
|
-
<div className={optionLabelTextClass}>{t("panel.config.gridTopForm")}</div>
|
|
2645
|
-
<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 } })} />
|
|
2646
|
-
</label>
|
|
2647
|
-
<label className="block space-y-1.5">
|
|
2648
|
-
<div className={optionLabelTextClass}>{t("panel.config.gridBottomForm")}</div>
|
|
2649
|
-
<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 } })} />
|
|
2650
|
-
</label>
|
|
2651
|
-
<label className="block space-y-1.5">
|
|
2652
|
-
<div className={optionLabelTextClass}>{t("panel.config.legendPositionForm")}</div>
|
|
2653
|
-
<Select value={String((selectedElement.chart?.option as any)?.legend?.top ?? "top")} onValueChange={(value) => updateSelectedOptionForm({ legend: { top: value } })}>
|
|
2654
|
-
<SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectLegendPosition")} /></SelectTrigger>
|
|
2655
|
-
<SelectContent>
|
|
2656
|
-
<SelectItem value="top">{t("panel.config.legendTop")}</SelectItem>
|
|
2657
|
-
<SelectItem value="bottom">{t("panel.config.legendBottom")}</SelectItem>
|
|
2658
|
-
<SelectItem value="left">{t("panel.config.legendLeft")}</SelectItem>
|
|
2659
|
-
<SelectItem value="right">{t("panel.config.legendRight")}</SelectItem>
|
|
2660
|
-
</SelectContent>
|
|
2661
|
-
</Select>
|
|
2662
|
-
</label>
|
|
2663
|
-
<label className="block space-y-1.5">
|
|
2664
|
-
<div className={optionLabelTextClass}>{t("panel.config.legendOrientForm")}</div>
|
|
2665
|
-
<Select value={String((selectedElement.chart?.option as any)?.legend?.orient ?? "horizontal")} onValueChange={(value) => updateSelectedOptionForm({ legend: { orient: value } })}>
|
|
2666
|
-
<SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectLegendOrient")} /></SelectTrigger>
|
|
2667
|
-
<SelectContent>
|
|
2668
|
-
<SelectItem value="horizontal">{t("panel.config.legendHorizontal")}</SelectItem>
|
|
2669
|
-
<SelectItem value="vertical">{t("panel.config.legendVertical")}</SelectItem>
|
|
2670
|
-
</SelectContent>
|
|
2671
|
-
</Select>
|
|
2672
|
-
</label>
|
|
2673
|
-
<label className="block space-y-1.5">
|
|
2674
|
-
{renderOptionLabel(t("panel.config.xAxisMin"), "xAxis.min", t("panel.config.xAxisMinHint"))}
|
|
2675
|
-
<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 } })} />
|
|
2676
|
-
</label>
|
|
2677
|
-
<label className="block space-y-1.5">
|
|
2678
|
-
{renderOptionLabel(t("panel.config.xAxisMax"), "xAxis.max", t("panel.config.xAxisMaxHint"))}
|
|
2679
|
-
<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 } })} />
|
|
2680
|
-
</label>
|
|
2681
|
-
<label className="block space-y-1.5">
|
|
2682
|
-
{renderOptionLabel(t("panel.config.yAxisMin"), "yAxis.min", t("panel.config.yAxisMinHint"))}
|
|
2683
|
-
<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 } })} />
|
|
2684
|
-
</label>
|
|
2685
|
-
<label className="block space-y-1.5">
|
|
2686
|
-
{renderOptionLabel(t("panel.config.yAxisMax"), "yAxis.max", t("panel.config.yAxisMaxHint"))}
|
|
2687
|
-
<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 } })} />
|
|
2688
|
-
</label>
|
|
2689
|
-
<label className="block space-y-1.5">
|
|
2690
|
-
{renderOptionLabel(t("panel.config.xAxisLabelRotate"), "xAxis.axisLabel.rotate", t("panel.config.rotateUnitHint"))}
|
|
2691
|
-
<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 } } })} />
|
|
2692
|
-
</label>
|
|
2693
|
-
<label className="block space-y-1.5">
|
|
2694
|
-
{renderOptionLabel(t("panel.config.yAxisLabelRotate"), "yAxis.axisLabel.rotate", t("panel.config.rotateUnitHint"))}
|
|
2695
|
-
<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 } } })} />
|
|
2696
|
-
</label>
|
|
2697
|
-
</div>
|
|
2698
|
-
</CollapsibleContent>
|
|
2699
|
-
</Collapsible>
|
|
2700
|
-
</div>
|
|
2701
|
-
<div className="rounded-lg border border-border/60 bg-background/70 p-3">
|
|
2702
|
-
<div className="space-y-2">
|
|
2703
|
-
<Collapsible
|
|
2704
|
-
open={isSectionExpanded("chartAdvancedHighFreq", true)}
|
|
2705
|
-
onOpenChange={(open) => setSectionExpanded("chartAdvancedHighFreq", open)}
|
|
2706
|
-
>
|
|
2707
|
-
<div className="mb-2 flex items-center gap-1.5">
|
|
2708
|
-
<CollapsibleTrigger asChild>
|
|
2709
|
-
<button
|
|
2710
|
-
type="button"
|
|
2711
|
-
className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
|
|
2712
|
-
aria-label={isSectionExpanded("chartAdvancedHighFreq", true) ? t("panel.config.collapseHighFreq") : t("panel.config.expandHighFreq")}
|
|
2713
|
-
>
|
|
2714
|
-
{isSectionExpanded("chartAdvancedHighFreq", true) ? "▾" : "▸"}
|
|
2715
|
-
</button>
|
|
2716
|
-
</CollapsibleTrigger>
|
|
2717
|
-
<div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupHighFreq")}</div>
|
|
2718
|
-
</div>
|
|
2719
|
-
<CollapsibleContent>
|
|
2720
|
-
<div className="grid grid-cols-1 gap-2">
|
|
2721
|
-
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2722
|
-
<Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.legend?.show ?? true)} onCheckedChange={(checked) => updateSelectedOptionForm({ legend: { show: checked === true } })} />
|
|
2723
|
-
{renderOptionLabel(t("panel.config.showLegend"), "legend.show", t("panel.config.showLegendHint"))}
|
|
2724
|
-
</label>
|
|
2725
|
-
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2726
|
-
<Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.grid?.containLabel ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ grid: { containLabel: checked === true } })} />
|
|
2727
|
-
{renderOptionLabel(t("panel.config.gridContainLabel"), "grid.containLabel", t("panel.config.gridContainLabelHint"))}
|
|
2728
|
-
</label>
|
|
2729
|
-
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2730
|
-
<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 }); }} />
|
|
2731
|
-
{renderOptionLabel(t("panel.config.zoomInside"), "dataZoom[type=inside]", t("panel.config.zoomInsideHint"))}
|
|
2732
|
-
</label>
|
|
2733
|
-
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2734
|
-
<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 }); }} />
|
|
2735
|
-
{renderOptionLabel(t("panel.config.zoomSlider"), "dataZoom[type=slider]", t("panel.config.zoomSliderHint"))}
|
|
2736
|
-
</label>
|
|
2737
|
-
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2738
|
-
<Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.animation ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ animation: checked === true })} />
|
|
2739
|
-
{renderOptionLabel(t("panel.config.enableAnimation"), "animation", t("panel.config.enableAnimationHint"))}
|
|
2740
|
-
</label>
|
|
2741
|
-
<label className="block space-y-1.5">
|
|
2742
|
-
{renderOptionLabel(t("panel.config.animationDuration"), "animationDuration", t("panel.config.animationDurationHint"))}
|
|
2743
|
-
<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) })} />
|
|
2744
|
-
</label>
|
|
2745
|
-
</div>
|
|
2746
|
-
</CollapsibleContent>
|
|
2747
|
-
</Collapsible>
|
|
2748
|
-
<Collapsible
|
|
2749
|
-
open={isSectionExpanded("chartAdvancedAxisPointer", false)}
|
|
2750
|
-
onOpenChange={(open) => setSectionExpanded("chartAdvancedAxisPointer", open)}
|
|
2751
|
-
>
|
|
2752
|
-
<div className="mb-2 flex items-center gap-1.5">
|
|
2753
|
-
<CollapsibleTrigger asChild>
|
|
2754
|
-
<button
|
|
2755
|
-
type="button"
|
|
2756
|
-
className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
|
|
2757
|
-
aria-label={isSectionExpanded("chartAdvancedAxisPointer", false) ? t("panel.config.collapseAxisPointer") : t("panel.config.expandAxisPointer")}
|
|
2758
|
-
>
|
|
2759
|
-
{isSectionExpanded("chartAdvancedAxisPointer", false) ? "▾" : "▸"}
|
|
2760
|
-
</button>
|
|
2761
|
-
</CollapsibleTrigger>
|
|
2762
|
-
<div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupAxisPointer")}</div>
|
|
2763
|
-
</div>
|
|
2764
|
-
<CollapsibleContent>
|
|
2765
|
-
<div className="grid grid-cols-1 gap-2">
|
|
2766
|
-
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2767
|
-
<Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.show ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { show: checked === true } })} />
|
|
2768
|
-
{renderOptionLabel(t("panel.config.showAxisPointer"), "axisPointer.show", t("panel.config.showAxisPointerHint"))}
|
|
2769
|
-
</label>
|
|
2770
|
-
<label className="block space-y-1.5 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2771
|
-
{renderOptionLabel(t("panel.config.axisPointerType"), "axisPointer.type", t("panel.config.axisPointerTypeHint"))}
|
|
2772
|
-
<Select value={String((selectedElement.chart?.option as any)?.axisPointer?.type ?? "line")} onValueChange={(value) => updateSelectedOptionForm({ axisPointer: { type: value } })}>
|
|
2773
|
-
<SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectAxisPointerType")} /></SelectTrigger>
|
|
2774
|
-
<SelectContent>
|
|
2775
|
-
<SelectItem value="line">{t("panel.config.axisPointerLine")}</SelectItem>
|
|
2776
|
-
<SelectItem value="shadow">{t("panel.config.axisPointerShadow")}</SelectItem>
|
|
2777
|
-
<SelectItem value="cross">{t("panel.config.axisPointerCross")}</SelectItem>
|
|
2778
|
-
</SelectContent>
|
|
2779
|
-
</Select>
|
|
2780
|
-
</label>
|
|
2781
|
-
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2782
|
-
<Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.snap ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { snap: checked === true } })} />
|
|
2783
|
-
{renderOptionLabel(t("panel.config.axisPointerSnap"), "axisPointer.snap", t("panel.config.axisPointerSnapHint"))}
|
|
2784
|
-
</label>
|
|
2785
|
-
</div>
|
|
2786
|
-
</CollapsibleContent>
|
|
2787
|
-
</Collapsible>
|
|
2788
|
-
</div>
|
|
2789
|
-
</div>
|
|
2790
|
-
</div>
|
|
2791
|
-
)}
|
|
2792
|
-
{renderFieldGroup(
|
|
2793
|
-
t("panel.config.groupJsonAdvanced"),
|
|
2794
|
-
<>
|
|
2795
|
-
<label className="flex items-center gap-2 rounded-md bg-background/70 px-2 py-1.5">
|
|
2796
|
-
<Checkbox
|
|
2797
|
-
checked={isAdvancedOptionMode}
|
|
2798
|
-
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"
|
|
2799
|
-
onCheckedChange={(checked) => setIsAdvancedOptionMode(checked === true)}
|
|
2800
|
-
/>
|
|
2801
|
-
<span>{t("panel.config.enableAdvancedJson")}</span>
|
|
2802
|
-
<ConfigHintIcon label={t("panel.config.advancedJsonHintLabel")}>
|
|
2803
|
-
{t("panel.config.advancedJsonHint")}
|
|
2804
|
-
</ConfigHintIcon>
|
|
2805
|
-
</label>
|
|
2806
|
-
{isAdvancedOptionMode ? (
|
|
2807
|
-
<>
|
|
2808
|
-
<Textarea
|
|
2809
|
-
value={optionJsonText}
|
|
2810
|
-
onChange={(e) => {
|
|
2811
|
-
const next = e.target.value;
|
|
2812
|
-
setOptionJsonText(next);
|
|
2813
|
-
try {
|
|
2814
|
-
const parsed = JSON.parse(next) as Record<string, unknown>;
|
|
2815
|
-
updateSelectedChart({ option: parsed });
|
|
2816
|
-
setOptionJsonError(null);
|
|
2817
|
-
} catch {
|
|
2818
|
-
setOptionJsonError(t("panel.config.jsonInvalid"));
|
|
2819
|
-
}
|
|
2820
|
-
}}
|
|
2821
|
-
spellCheck={false}
|
|
2822
|
-
className="h-44 font-mono text-[11px]"
|
|
2823
|
-
/>
|
|
2824
|
-
{optionJsonError ? (
|
|
2825
|
-
<div className="rounded border border-destructive/40 bg-destructive/10 px-2 py-1.5 text-[11px] text-destructive">
|
|
2826
|
-
{optionJsonError}
|
|
2827
|
-
</div>
|
|
2828
|
-
) : (
|
|
2829
|
-
<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">
|
|
2830
|
-
{t("panel.config.jsonValidApplied")}
|
|
2831
|
-
</div>
|
|
2832
|
-
)}
|
|
2833
|
-
</>
|
|
2834
|
-
) : null}
|
|
2835
|
-
</>
|
|
2836
|
-
)}
|
|
2837
|
-
</>,
|
|
2838
|
-
false,
|
|
2839
|
-
["json", "option", t("panel.config.searchKwAdvanced"), "echarts"]
|
|
2840
|
-
)}
|
|
2841
|
-
</>
|
|
2842
|
-
) : selectedElement.materialType === "table" ? (
|
|
2843
|
-
renderSection(
|
|
2844
|
-
"tableConfig",
|
|
2845
|
-
t("panel.config.sectionTable"),
|
|
2846
|
-
<PanelConfigTableSection
|
|
2847
|
-
element={selectedElement}
|
|
2848
|
-
disabled={!isNodeEditable}
|
|
2849
|
-
updateElement={updateElement}
|
|
2850
|
-
blueprintNodeOptions={blueprintNodeOptions}
|
|
2851
|
-
/>,
|
|
2852
|
-
true,
|
|
2853
|
-
[
|
|
2854
|
-
t("panel.material.table"),
|
|
2855
|
-
"table",
|
|
2856
|
-
t("panel.config.tableSource"),
|
|
2857
|
-
t("panel.config.tableColumns"),
|
|
2858
|
-
t("panel.config.tableRowsText"),
|
|
2859
|
-
]
|
|
2860
|
-
)
|
|
2861
|
-
) : selectedElement.materialType === "text" ? (
|
|
2862
|
-
renderSection(
|
|
2863
|
-
"textConfig",
|
|
2864
|
-
t("panel.config.sectionText"),
|
|
2865
|
-
<>
|
|
2866
|
-
{renderFieldGroup(
|
|
2867
|
-
t("panel.config.groupTextContent"),
|
|
2868
|
-
<>
|
|
2869
|
-
<div className="flex items-center gap-1">
|
|
2870
|
-
<button
|
|
2871
|
-
type="button"
|
|
2872
|
-
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
|
|
2873
|
-
onClick={() => execTextCommand("bold")}
|
|
2874
|
-
>
|
|
2875
|
-
B
|
|
2876
|
-
</button>
|
|
2877
|
-
<button
|
|
2878
|
-
type="button"
|
|
2879
|
-
className="rounded border border-border px-2 py-1 text-[11px] italic hover:bg-accent"
|
|
2880
|
-
onClick={() => execTextCommand("italic")}
|
|
2881
|
-
>
|
|
2882
|
-
I
|
|
2883
|
-
</button>
|
|
2884
|
-
<button
|
|
2885
|
-
type="button"
|
|
2886
|
-
className="rounded border border-border px-2 py-1 text-[11px] underline hover:bg-accent"
|
|
2887
|
-
onClick={() => execTextCommand("underline")}
|
|
2888
|
-
>
|
|
2889
|
-
U
|
|
2890
|
-
</button>
|
|
2891
|
-
</div>
|
|
2892
|
-
<div
|
|
2893
|
-
ref={textEditorRef}
|
|
2894
|
-
data-config-field="textHtml"
|
|
2895
|
-
data-panel-user-text=""
|
|
2896
|
-
className="min-h-[120px] rounded border border-border bg-background px-2 py-1.5 leading-6 outline-none"
|
|
2897
|
-
style={{
|
|
2898
|
-
fontFamily: selectedElement.textFontFamily || undefined,
|
|
2899
|
-
fontSize: selectedElement.textFontSize
|
|
2900
|
-
? `${selectedElement.textFontSize}px`
|
|
2901
|
-
: undefined,
|
|
2902
|
-
fontWeight: selectedElement.textFontWeight || undefined,
|
|
2903
|
-
color: selectedElement.textColor || undefined,
|
|
2904
|
-
lineHeight: cssTextLineHeight(selectedElement.textLineHeight),
|
|
2905
|
-
...cssTextAlignStyle(selectedElement.textAlign),
|
|
2906
|
-
}}
|
|
2907
|
-
contentEditable
|
|
2908
|
-
suppressContentEditableWarning
|
|
2909
|
-
onInput={(e) => {
|
|
2910
|
-
const nextHtml = (e.currentTarget as HTMLDivElement).innerHTML;
|
|
2911
|
-
updateSelectedText({
|
|
2912
|
-
textHtml: nextHtml || "<p><br/></p>",
|
|
2913
|
-
});
|
|
2914
|
-
}}
|
|
2915
|
-
/>
|
|
2916
|
-
</>
|
|
2917
|
-
)}
|
|
2918
|
-
{renderFieldGroup(
|
|
2919
|
-
t("panel.config.groupTextStyle"),
|
|
2920
|
-
<>
|
|
2921
|
-
<label className="block space-y-1">
|
|
2922
|
-
<div>{t("panel.config.fontFamily")}</div>
|
|
2923
|
-
<Input
|
|
2924
|
-
value={selectedElement.textFontFamily ?? ""}
|
|
2925
|
-
onChange={(e) =>
|
|
2926
|
-
updateSelectedText({
|
|
2927
|
-
textFontFamily: e.target.value || undefined,
|
|
2928
|
-
})
|
|
2929
|
-
}
|
|
2930
|
-
placeholder={t("panel.config.fontFamilyPlaceholder")}
|
|
2931
|
-
className="h-7"
|
|
2932
|
-
/>
|
|
2933
|
-
</label>
|
|
2934
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2935
|
-
<label className="block space-y-1">
|
|
2936
|
-
<div>{t("panel.config.fontSizePx")}</div>
|
|
2937
|
-
<Input
|
|
2938
|
-
type="number"
|
|
2939
|
-
min={8}
|
|
2940
|
-
max={200}
|
|
2941
|
-
step={1}
|
|
2942
|
-
value={selectedElement.textFontSize ?? 14}
|
|
2943
|
-
onChange={(e) =>
|
|
2944
|
-
updateSelectedText({
|
|
2945
|
-
textFontSize: Math.max(8, Number(e.target.value) || 14),
|
|
2946
|
-
})
|
|
2947
|
-
}
|
|
2948
|
-
className="h-7"
|
|
2949
|
-
/>
|
|
2950
|
-
</label>
|
|
2951
|
-
<label className="block space-y-1">
|
|
2952
|
-
<div>{t("panel.config.fontWeight")}</div>
|
|
2953
|
-
<Select
|
|
2954
|
-
value={selectedElement.textFontWeight ?? "400"}
|
|
2955
|
-
onValueChange={(value) =>
|
|
2956
|
-
updateSelectedText({
|
|
2957
|
-
textFontWeight: value,
|
|
2958
|
-
})
|
|
2959
|
-
}
|
|
2960
|
-
>
|
|
2961
|
-
<SelectTrigger className="h-7">
|
|
2962
|
-
<SelectValue placeholder={t("panel.config.selectFontWeight")} />
|
|
2963
|
-
</SelectTrigger>
|
|
2964
|
-
<SelectContent>
|
|
2965
|
-
<SelectItem value="300">300</SelectItem>
|
|
2966
|
-
<SelectItem value="400">400</SelectItem>
|
|
2967
|
-
<SelectItem value="500">500</SelectItem>
|
|
2968
|
-
<SelectItem value="600">600</SelectItem>
|
|
2969
|
-
<SelectItem value="700">700</SelectItem>
|
|
2970
|
-
</SelectContent>
|
|
2971
|
-
</Select>
|
|
2972
|
-
</label>
|
|
2973
|
-
</div>
|
|
2974
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2975
|
-
<label className="block space-y-1">
|
|
2976
|
-
<div>{t("panel.config.textAlign")}</div>
|
|
2977
|
-
<Select
|
|
2978
|
-
value={selectedElement.textAlign ?? "left"}
|
|
2979
|
-
onValueChange={(value) =>
|
|
2980
|
-
updateSelectedText({
|
|
2981
|
-
textAlign: value as PanelElement["textAlign"],
|
|
2982
|
-
})
|
|
2983
|
-
}
|
|
2984
|
-
>
|
|
2985
|
-
<SelectTrigger className="h-7">
|
|
2986
|
-
<SelectValue placeholder={t("panel.config.selectTextAlign")} />
|
|
2987
|
-
</SelectTrigger>
|
|
2988
|
-
<SelectContent>
|
|
2989
|
-
<SelectItem value="left">{t("panel.config.alignLeft")}</SelectItem>
|
|
2990
|
-
<SelectItem value="center">{t("panel.config.alignCenter")}</SelectItem>
|
|
2991
|
-
<SelectItem value="middle">{t("panel.config.alignMiddle")}</SelectItem>
|
|
2992
|
-
<SelectItem value="right">{t("panel.config.alignRight")}</SelectItem>
|
|
2993
|
-
<SelectItem value="justify">{t("panel.config.alignJustify")}</SelectItem>
|
|
2994
|
-
</SelectContent>
|
|
2995
|
-
</Select>
|
|
2996
|
-
</label>
|
|
2997
|
-
<label className="block space-y-1">
|
|
2998
|
-
<div className="flex items-center justify-between gap-2">
|
|
2999
|
-
<span>{t("panel.config.lineHeight")}</span>
|
|
3000
|
-
<label className="flex items-center gap-1 font-normal">
|
|
3001
|
-
<Checkbox
|
|
3002
|
-
checked={selectedElement.textLineHeight === "auto"}
|
|
3003
|
-
className="h-3.5 w-3.5 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
3004
|
-
onCheckedChange={(checked) =>
|
|
3005
|
-
updateSelectedText({
|
|
3006
|
-
textLineHeight: checked === true ? "auto" : 1.6,
|
|
3007
|
-
})
|
|
3008
|
-
}
|
|
3009
|
-
/>
|
|
3010
|
-
<span>{t("panel.config.lineHeightAuto")}</span>
|
|
3011
|
-
</label>
|
|
3012
|
-
</div>
|
|
3013
|
-
<Input
|
|
3014
|
-
type="number"
|
|
3015
|
-
min={1}
|
|
3016
|
-
max={3}
|
|
3017
|
-
step={0.1}
|
|
3018
|
-
disabled={selectedElement.textLineHeight === "auto"}
|
|
3019
|
-
placeholder={t("panel.config.lineHeightAuto")}
|
|
3020
|
-
value={
|
|
3021
|
-
selectedElement.textLineHeight === "auto"
|
|
3022
|
-
? ""
|
|
3023
|
-
: (selectedElement.textLineHeight ?? 1.6)
|
|
3024
|
-
}
|
|
3025
|
-
onChange={(e) =>
|
|
3026
|
-
updateSelectedText({
|
|
3027
|
-
textLineHeight: Math.min(
|
|
3028
|
-
3,
|
|
3029
|
-
Math.max(1, Number(e.target.value) || 1.6)
|
|
3030
|
-
),
|
|
3031
|
-
})
|
|
3032
|
-
}
|
|
3033
|
-
className="h-7"
|
|
3034
|
-
/>
|
|
3035
|
-
</label>
|
|
3036
|
-
</div>
|
|
3037
|
-
{renderColorField(
|
|
3038
|
-
t("panel.config.textColor"),
|
|
3039
|
-
selectedElement.textColor ?? "",
|
|
3040
|
-
(next) =>
|
|
3041
|
-
updateSelectedText({
|
|
3042
|
-
textColor: next || undefined,
|
|
3043
|
-
})
|
|
3044
|
-
)}
|
|
3045
|
-
</>
|
|
3046
|
-
)}
|
|
3047
|
-
{renderFieldGroup(
|
|
3048
|
-
t("panel.config.groupInputAbility"),
|
|
3049
|
-
<label className="flex items-center gap-2">
|
|
3050
|
-
<Checkbox
|
|
3051
|
-
checked={selectedElement.textAllowInput ?? true}
|
|
3052
|
-
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"
|
|
3053
|
-
onCheckedChange={(checked) =>
|
|
3054
|
-
updateSelectedText({
|
|
3055
|
-
textAllowInput: checked !== false,
|
|
3056
|
-
})
|
|
3057
|
-
}
|
|
3058
|
-
/>
|
|
3059
|
-
<span>{t("panel.config.allowCanvasInput")}</span>
|
|
3060
|
-
</label>,
|
|
3061
|
-
undefined,
|
|
3062
|
-
{ groupKey: "textInputAbility", defaultOpen: false }
|
|
3063
|
-
)}
|
|
3064
|
-
</>,
|
|
3065
|
-
true,
|
|
3066
|
-
[t("panel.material.text"), t("panel.config.searchKwRichText"), t("panel.config.fontFamily"), t("panel.config.color"), t("panel.config.textAlign"), t("panel.config.lineHeight"), t("panel.config.searchKwInput")]
|
|
3067
|
-
)
|
|
3068
|
-
) : selectedElement.materialType === "audio" ? (
|
|
3069
|
-
renderSection(
|
|
3070
|
-
"audioConfig",
|
|
3071
|
-
t("panel.config.sectionAudio"),
|
|
3072
|
-
<>
|
|
3073
|
-
{renderFieldGroup(
|
|
3074
|
-
t("panel.config.groupAudioSource"),
|
|
3075
|
-
<>
|
|
3076
|
-
<label className="block space-y-1">
|
|
3077
|
-
<div>{t("panel.config.audioUrl")}</div>
|
|
3078
|
-
<Input
|
|
3079
|
-
value={selectedElement.audioRemoteUrl ?? ""}
|
|
3080
|
-
onChange={(e) =>
|
|
3081
|
-
updateSelectedAudio({
|
|
3082
|
-
audioRemoteUrl: e.target.value || undefined,
|
|
3083
|
-
audioSrc: e.target.value || selectedElement.audioSrc,
|
|
3084
|
-
})
|
|
3085
|
-
}
|
|
3086
|
-
placeholder="https://example.com/audio.mp3"
|
|
3087
|
-
className="h-7"
|
|
3088
|
-
/>
|
|
3089
|
-
</label>
|
|
3090
|
-
<div className="flex items-center gap-2">
|
|
3091
|
-
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
3092
|
-
{t("panel.config.uploadAudio")}
|
|
3093
|
-
<Input
|
|
3094
|
-
type="file"
|
|
3095
|
-
accept="audio/*"
|
|
3096
|
-
className="hidden"
|
|
3097
|
-
onChange={async (e) => {
|
|
3098
|
-
const file = e.target.files?.[0];
|
|
3099
|
-
e.currentTarget.value = "";
|
|
3100
|
-
if (!file) return;
|
|
3101
|
-
await handleUploadAudioFile(file);
|
|
3102
|
-
}}
|
|
3103
|
-
/>
|
|
3104
|
-
</label>
|
|
3105
|
-
<button
|
|
3106
|
-
type="button"
|
|
3107
|
-
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent disabled:opacity-50"
|
|
3108
|
-
onClick={isRecordingAudio ? stopRecordingAudio : startRecordingAudio}
|
|
3109
|
-
>
|
|
3110
|
-
{isRecordingAudio ? t("panel.config.stopRecord") : t("panel.config.startRecord")}
|
|
3111
|
-
</button>
|
|
3112
|
-
</div>
|
|
3113
|
-
{audioStatus ? (
|
|
3114
|
-
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3115
|
-
{audioStatus}
|
|
3116
|
-
</div>
|
|
3117
|
-
) : null}
|
|
3118
|
-
<audio
|
|
3119
|
-
controls
|
|
3120
|
-
className="h-8 w-full"
|
|
3121
|
-
src={selectedElement.audioSrc || selectedElement.audioRemoteUrl || ""}
|
|
3122
|
-
/>
|
|
3123
|
-
</>
|
|
3124
|
-
)}
|
|
3125
|
-
{renderFieldGroup(
|
|
3126
|
-
t("panel.config.groupDisplayStyle"),
|
|
3127
|
-
<>
|
|
3128
|
-
<label className="flex items-center gap-2">
|
|
3129
|
-
<Checkbox
|
|
3130
|
-
checked={selectedElement.mediaAutoPauseOnEdit !== false}
|
|
3131
|
-
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"
|
|
3132
|
-
onCheckedChange={(checked) =>
|
|
3133
|
-
updateSelectedAudio({
|
|
3134
|
-
mediaAutoPauseOnEdit: checked !== false,
|
|
3135
|
-
})
|
|
3136
|
-
}
|
|
3137
|
-
/>
|
|
3138
|
-
<span>{t("panel.config.autoPauseMedia")}</span>
|
|
3139
|
-
</label>
|
|
3140
|
-
<label className="block space-y-1">
|
|
3141
|
-
<div>{t("panel.config.audioIconPreset")}</div>
|
|
3142
|
-
<Select
|
|
3143
|
-
value={selectedElement.audioIconPreset ?? "__none__"}
|
|
3144
|
-
onValueChange={(value) =>
|
|
3145
|
-
updateSelectedAudio({
|
|
3146
|
-
audioIconPreset:
|
|
3147
|
-
value === "__none__"
|
|
3148
|
-
? undefined
|
|
3149
|
-
: (value as "speaker" | "music" | "headphone" | "wave"),
|
|
3150
|
-
})
|
|
3151
|
-
}
|
|
3152
|
-
>
|
|
3153
|
-
<SelectTrigger className="h-7">
|
|
3154
|
-
<SelectValue placeholder={t("panel.config.selectIcon")} />
|
|
3155
|
-
</SelectTrigger>
|
|
3156
|
-
<SelectContent>
|
|
3157
|
-
<SelectItem value="__none__">{t("panel.config.iconDefaultProgress")}</SelectItem>
|
|
3158
|
-
<SelectItem value="speaker">{t("panel.config.iconSpeaker")}</SelectItem>
|
|
3159
|
-
<SelectItem value="music">{t("panel.config.iconMusic")}</SelectItem>
|
|
3160
|
-
<SelectItem value="headphone">{t("panel.config.iconHeadphone")}</SelectItem>
|
|
3161
|
-
<SelectItem value="wave">{t("panel.config.iconWave")}</SelectItem>
|
|
3162
|
-
</SelectContent>
|
|
3163
|
-
</Select>
|
|
3164
|
-
</label>
|
|
3165
|
-
<label className="block space-y-1">
|
|
3166
|
-
<div>{t("panel.config.visualEffect")}</div>
|
|
3167
|
-
<Select
|
|
3168
|
-
value={selectedElement.audioVisualEffect ?? "pulse"}
|
|
3169
|
-
onValueChange={(value) =>
|
|
3170
|
-
updateSelectedAudio({
|
|
3171
|
-
audioVisualEffect: value as "none" | "pulse" | "ripple",
|
|
3172
|
-
})
|
|
3173
|
-
}
|
|
3174
|
-
>
|
|
3175
|
-
<SelectTrigger className="h-7">
|
|
3176
|
-
<SelectValue placeholder={t("panel.config.selectEffect")} />
|
|
3177
|
-
</SelectTrigger>
|
|
3178
|
-
<SelectContent>
|
|
3179
|
-
<SelectItem value="none">{t("panel.config.effectNone")}</SelectItem>
|
|
3180
|
-
<SelectItem value="pulse">{t("panel.config.effectPulse")}</SelectItem>
|
|
3181
|
-
<SelectItem value="ripple">{t("panel.config.effectRipple")}</SelectItem>
|
|
3182
|
-
</SelectContent>
|
|
3183
|
-
</Select>
|
|
3184
|
-
</label>
|
|
3185
|
-
<label className="block space-y-1">
|
|
3186
|
-
<div>{t("panel.config.effectSpeed")}</div>
|
|
3187
|
-
<Select
|
|
3188
|
-
value={selectedElement.audioVisualSpeed ?? "normal"}
|
|
3189
|
-
onValueChange={(value) =>
|
|
3190
|
-
updateSelectedAudio({
|
|
3191
|
-
audioVisualSpeed: value as "slow" | "normal" | "fast",
|
|
3192
|
-
})
|
|
3193
|
-
}
|
|
3194
|
-
>
|
|
3195
|
-
<SelectTrigger className="h-7">
|
|
3196
|
-
<SelectValue placeholder={t("panel.config.selectSpeed")} />
|
|
3197
|
-
</SelectTrigger>
|
|
3198
|
-
<SelectContent>
|
|
3199
|
-
<SelectItem value="slow">{t("panel.config.speedSlow")}</SelectItem>
|
|
3200
|
-
<SelectItem value="normal">{t("panel.config.speedNormal")}</SelectItem>
|
|
3201
|
-
<SelectItem value="fast">{t("panel.config.speedFast")}</SelectItem>
|
|
3202
|
-
</SelectContent>
|
|
3203
|
-
</Select>
|
|
3204
|
-
</label>
|
|
3205
|
-
<div className="flex items-center gap-2">
|
|
3206
|
-
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
3207
|
-
{t("panel.config.uploadPoster")}
|
|
3208
|
-
<Input
|
|
3209
|
-
type="file"
|
|
3210
|
-
accept="image/*"
|
|
3211
|
-
className="hidden"
|
|
3212
|
-
onChange={async (e) => {
|
|
3213
|
-
const file = e.target.files?.[0];
|
|
3214
|
-
e.currentTarget.value = "";
|
|
3215
|
-
if (!file) return;
|
|
3216
|
-
await handleUploadAudioPoster(file);
|
|
3217
|
-
}}
|
|
3218
|
-
/>
|
|
3219
|
-
</label>
|
|
3220
|
-
<button
|
|
3221
|
-
type="button"
|
|
3222
|
-
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
|
|
3223
|
-
onClick={() =>
|
|
3224
|
-
updateSelectedAudio({
|
|
3225
|
-
audioPosterImage: undefined,
|
|
3226
|
-
})
|
|
3227
|
-
}
|
|
3228
|
-
>
|
|
3229
|
-
{t("panel.config.clearPoster")}
|
|
3230
|
-
</button>
|
|
3231
|
-
</div>
|
|
3232
|
-
{selectedElement.audioPosterImage ? (
|
|
3233
|
-
<img
|
|
3234
|
-
src={selectedElement.audioPosterImage}
|
|
3235
|
-
alt={t("panel.config.audioPosterAlt")}
|
|
3236
|
-
className="h-20 w-full rounded border border-border/60 object-cover"
|
|
3237
|
-
/>
|
|
3238
|
-
) : null}
|
|
3239
|
-
<div className="flex items-center gap-1">
|
|
3240
|
-
<div className="text-[11px] text-muted-foreground">{t("panel.config.audioPoster")}</div>
|
|
3241
|
-
<ConfigHintIcon label={t("panel.config.audioPoster")}>
|
|
3242
|
-
{t("panel.config.audioPosterHint")}
|
|
3243
|
-
</ConfigHintIcon>
|
|
3244
|
-
</div>
|
|
3245
|
-
</>,
|
|
3246
|
-
undefined,
|
|
3247
|
-
{ groupKey: "audioDisplayStyle", defaultOpen: false }
|
|
3248
|
-
)}
|
|
3249
|
-
</>,
|
|
3250
|
-
true,
|
|
3251
|
-
[t("panel.material.audio"), "url", t("panel.config.searchKwUpload"), t("panel.config.searchKwRecord"), "icon", t("panel.config.searchKwPoster"), t("panel.config.effect"), t("panel.config.searchKwAutoPause"), "media"]
|
|
3252
|
-
)
|
|
3253
|
-
) : selectedElement.materialType === "video" ? (
|
|
3254
|
-
renderSection(
|
|
3255
|
-
"videoConfig",
|
|
3256
|
-
t("panel.config.sectionVideo"),
|
|
3257
|
-
<>
|
|
3258
|
-
{renderFieldGroup(
|
|
3259
|
-
t("panel.config.groupVideoSource"),
|
|
3260
|
-
<>
|
|
3261
|
-
<label className="block space-y-1">
|
|
3262
|
-
<div>{t("panel.config.videoUrl")}</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
|
-
{t("panel.config.uploadVideo")}
|
|
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>{t("panel.config.autoPauseMedia")}</span>
|
|
3312
|
-
</label>
|
|
3313
|
-
</>
|
|
3314
|
-
)}
|
|
3315
|
-
</>,
|
|
3316
|
-
true,
|
|
3317
|
-
[t("panel.material.video"), "url", t("panel.config.searchKwUpload"), t("panel.config.searchKwPreview"), t("panel.config.searchKwAutoPause"), "media"]
|
|
3318
|
-
)
|
|
3319
|
-
) : null}
|
|
3320
|
-
{selectedElement
|
|
3321
|
-
? selectedElement.parentGridId
|
|
3322
|
-
? renderSection(
|
|
3323
|
-
"gridChildSpan",
|
|
3324
|
-
t("panel.config.sectionGridChildSpan"),
|
|
3325
|
-
<>
|
|
3326
|
-
{renderFieldGroup(
|
|
3327
|
-
t("panel.config.groupCrossSlots"),
|
|
3328
|
-
<div className="grid grid-cols-2 gap-2">
|
|
3329
|
-
<label className="block space-y-1">
|
|
3330
|
-
<div>{t("panel.config.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>{t("panel.config.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
|
-
</>,
|
|
3362
|
-
true,
|
|
3363
|
-
[t("panel.config.searchKwGrid"), t("panel.config.searchKwCrossCol"), t("panel.config.searchKwCrossRow"), "span", "slot"],
|
|
3364
|
-
<>{t("panel.config.gridChildHint")}</>
|
|
3365
|
-
)
|
|
3366
|
-
: null
|
|
3367
|
-
: null}
|
|
3368
|
-
{selectedElement.materialType === "geometry" ? (
|
|
3369
|
-
renderSection(
|
|
3370
|
-
"geometryConfig",
|
|
3371
|
-
t("panel.config.sectionGeometry"),
|
|
3372
|
-
<>
|
|
3373
|
-
{renderFieldGroup(
|
|
3374
|
-
t("panel.config.groupBasicShape"),
|
|
3375
|
-
<>
|
|
3376
|
-
<label className="block space-y-1">
|
|
3377
|
-
<div>{t("panel.config.shape")}</div>
|
|
3378
|
-
<Select
|
|
3379
|
-
value={selectedElement.geometryShape ?? "rect"}
|
|
3380
|
-
onValueChange={(value) =>
|
|
3381
|
-
updateSelectedGeometry({
|
|
3382
|
-
geometryShape: value as PanelElement["geometryShape"],
|
|
3383
|
-
})
|
|
3384
|
-
}
|
|
3385
|
-
>
|
|
3386
|
-
<SelectTrigger className="h-7">
|
|
3387
|
-
<SelectValue placeholder={t("panel.config.selectShape")} />
|
|
3388
|
-
</SelectTrigger>
|
|
3389
|
-
<SelectContent>
|
|
3390
|
-
<SelectItem value="rect">{t("panel.config.shapeRect")}</SelectItem>
|
|
3391
|
-
<SelectItem value="circle">{t("panel.config.shapeCircle")}</SelectItem>
|
|
3392
|
-
<SelectItem value="triangle">{t("panel.config.shapeTriangle")}</SelectItem>
|
|
3393
|
-
<SelectItem value="diamond">{t("panel.config.shapeDiamond")}</SelectItem>
|
|
3394
|
-
<SelectItem value="hexagon">{t("panel.config.shapeHexagon")}</SelectItem>
|
|
3395
|
-
<SelectItem value="star">{t("panel.config.shapeStar")}</SelectItem>
|
|
3396
|
-
<SelectItem value="heart">{t("panel.config.shapeHeart")}</SelectItem>
|
|
3397
|
-
</SelectContent>
|
|
3398
|
-
</Select>
|
|
3399
|
-
</label>
|
|
3400
|
-
{renderColorField(
|
|
3401
|
-
t("panel.config.geometryColor"),
|
|
3402
|
-
selectedElement.geometryColor ?? "#3b82f6",
|
|
3403
|
-
(next) => updateSelectedGeometry({ geometryColor: next || "#3b82f6" })
|
|
3404
|
-
)}
|
|
3405
|
-
</>
|
|
3406
|
-
)}
|
|
3407
|
-
{renderFieldGroup(
|
|
3408
|
-
t("panel.config.groupCanvasScript"),
|
|
3409
|
-
<>
|
|
3410
|
-
<Textarea
|
|
3411
|
-
value={selectedElement.geometryScript ?? ""}
|
|
3412
|
-
onChange={(e) => updateSelectedGeometry({ geometryScript: e.target.value || undefined })}
|
|
3413
|
-
spellCheck={false}
|
|
3414
|
-
className="h-36 font-mono text-[11px]"
|
|
3415
|
-
placeholder={t("panel.config.geometryScriptPlaceholder")}
|
|
3416
|
-
/>
|
|
3417
|
-
</>,
|
|
3418
|
-
<>
|
|
3419
|
-
{t("panel.config.canvasScriptHint")}
|
|
3420
|
-
</>,
|
|
3421
|
-
{ groupKey: "geometryCanvasScript", defaultOpen: false }
|
|
3422
|
-
)}
|
|
3423
|
-
{renderFieldGroup(
|
|
3424
|
-
t("panel.config.groupSketchOverlay"),
|
|
3425
|
-
<>
|
|
3426
|
-
<div className="flex items-center gap-2">
|
|
3427
|
-
<label className="block space-y-1">
|
|
3428
|
-
<div className="text-[11px]">{t("panel.config.penColor")}</div>
|
|
3429
|
-
<Input
|
|
3430
|
-
type="color"
|
|
3431
|
-
value={geometryDrawPenColor}
|
|
3432
|
-
onChange={(e) => setGeometryDrawPenColor(e.target.value)}
|
|
3433
|
-
className="h-7 w-10 p-1"
|
|
3434
|
-
/>
|
|
3435
|
-
</label>
|
|
3436
|
-
<label className="block space-y-1">
|
|
3437
|
-
<div className="text-[11px]">{t("panel.config.penWidth")}</div>
|
|
3438
|
-
<Input
|
|
3439
|
-
type="number"
|
|
3440
|
-
min={1}
|
|
3441
|
-
max={24}
|
|
3442
|
-
value={geometryDrawPenWidth}
|
|
3443
|
-
onChange={(e) => setGeometryDrawPenWidth(Math.max(1, Math.min(24, Number(e.target.value) || 1)))}
|
|
3444
|
-
className="h-7 w-20"
|
|
3445
|
-
/>
|
|
3446
|
-
</label>
|
|
3447
|
-
</div>
|
|
3448
|
-
<div className="rounded border border-border/60 bg-white p-2">
|
|
3449
|
-
<canvas
|
|
3450
|
-
ref={geometryDrawCanvasRef}
|
|
3451
|
-
width={320}
|
|
3452
|
-
height={180}
|
|
3453
|
-
className="h-[180px] w-full cursor-crosshair rounded border border-border/60"
|
|
3454
|
-
onPointerDown={(e) => {
|
|
3455
|
-
const canvas = geometryDrawCanvasRef.current;
|
|
3456
|
-
if (!canvas) return;
|
|
3457
|
-
const ctx = canvas.getContext("2d");
|
|
3458
|
-
if (!ctx) return;
|
|
3459
|
-
const rect = canvas.getBoundingClientRect();
|
|
3460
|
-
const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
|
|
3461
|
-
const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
|
|
3462
|
-
geometryLastPointRef.current = { x, y };
|
|
3463
|
-
setIsGeometryDrawing(true);
|
|
3464
|
-
ctx.lineCap = "round";
|
|
3465
|
-
ctx.lineJoin = "round";
|
|
3466
|
-
ctx.strokeStyle = geometryDrawPenColor;
|
|
3467
|
-
ctx.lineWidth = geometryDrawPenWidth;
|
|
3468
|
-
ctx.beginPath();
|
|
3469
|
-
ctx.moveTo(x, y);
|
|
3470
|
-
}}
|
|
3471
|
-
onPointerMove={(e) => {
|
|
3472
|
-
if (!isGeometryDrawing) return;
|
|
3473
|
-
const canvas = geometryDrawCanvasRef.current;
|
|
3474
|
-
const last = geometryLastPointRef.current;
|
|
3475
|
-
if (!canvas || !last) return;
|
|
3476
|
-
const ctx = canvas.getContext("2d");
|
|
3477
|
-
if (!ctx) return;
|
|
3478
|
-
const rect = canvas.getBoundingClientRect();
|
|
3479
|
-
const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
|
|
3480
|
-
const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
|
|
3481
|
-
ctx.lineTo(x, y);
|
|
3482
|
-
ctx.stroke();
|
|
3483
|
-
geometryLastPointRef.current = { x, y };
|
|
3484
|
-
}}
|
|
3485
|
-
onPointerUp={() => {
|
|
3486
|
-
setIsGeometryDrawing(false);
|
|
3487
|
-
geometryLastPointRef.current = null;
|
|
3488
|
-
}}
|
|
3489
|
-
onPointerLeave={() => {
|
|
3490
|
-
setIsGeometryDrawing(false);
|
|
3491
|
-
geometryLastPointRef.current = null;
|
|
3492
|
-
}}
|
|
3493
|
-
/>
|
|
3494
|
-
</div>
|
|
3495
|
-
<div className="flex items-center gap-2">
|
|
3496
|
-
<button
|
|
3497
|
-
type="button"
|
|
3498
|
-
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
|
|
3499
|
-
onClick={() => {
|
|
3500
|
-
const canvas = geometryDrawCanvasRef.current;
|
|
3501
|
-
if (!canvas) return;
|
|
3502
|
-
updateSelectedGeometry({ geometrySketchDataUrl: canvas.toDataURL("image/png") });
|
|
3503
|
-
}}
|
|
3504
|
-
>
|
|
3505
|
-
{t("panel.config.applySketch")}
|
|
3506
|
-
</button>
|
|
3507
|
-
<button
|
|
3508
|
-
type="button"
|
|
3509
|
-
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
|
|
3510
|
-
onClick={() => {
|
|
3511
|
-
updateSelectedGeometry({ geometrySketchDataUrl: undefined });
|
|
3512
|
-
redrawGeometryPadFromElement();
|
|
3513
|
-
}}
|
|
3514
|
-
>
|
|
3515
|
-
{t("panel.config.clearSketch")}
|
|
3516
|
-
</button>
|
|
3517
|
-
</div>
|
|
3518
|
-
</>,
|
|
3519
|
-
undefined,
|
|
3520
|
-
{ groupKey: "geometrySketch", defaultOpen: false }
|
|
3521
|
-
)}
|
|
3522
|
-
</>,
|
|
3523
|
-
true,
|
|
3524
|
-
[t("panel.material.geometry"), "geometry", t("panel.config.shape"), "canvas", t("panel.config.searchKwScript"), t("panel.config.searchKwSketch")]
|
|
3525
|
-
)
|
|
3526
|
-
) : selectedElement.materialType === "scene3d" ? (
|
|
3527
|
-
renderSection(
|
|
3528
|
-
"scene3dConfig",
|
|
3529
|
-
t("panel.config.sectionScene3d"),
|
|
3530
|
-
<PanelConfigScene3dSection
|
|
3531
|
-
element={selectedElement}
|
|
3532
|
-
updateElement={updateElement}
|
|
3533
|
-
/>,
|
|
3534
|
-
true,
|
|
3535
|
-
[
|
|
3536
|
-
t("panel.material.scene3d"),
|
|
3537
|
-
"3d",
|
|
3538
|
-
"glb",
|
|
3539
|
-
t("panel.config.scene3dUploadModels"),
|
|
3540
|
-
t("panel.config.groupScene3dCamera"),
|
|
3541
|
-
t("panel.config.groupScene3dModelAnim"),
|
|
3542
|
-
]
|
|
3543
|
-
)
|
|
3544
|
-
) : selectedElement.materialType === "grid" ? (
|
|
3545
|
-
renderSection(
|
|
3546
|
-
"gridConfig",
|
|
3547
|
-
t("panel.config.sectionGrid"),
|
|
3548
|
-
<>
|
|
3549
|
-
{renderFieldGroup(
|
|
3550
|
-
t("panel.config.groupGridParams"),
|
|
3551
|
-
<div className="grid grid-cols-2 gap-2">
|
|
3552
|
-
<label className="block space-y-1">
|
|
3553
|
-
<div>{t("panel.config.rowCount")}</div>
|
|
3554
|
-
<Input
|
|
3555
|
-
type="number"
|
|
3556
|
-
min={1}
|
|
3557
|
-
max={12}
|
|
3558
|
-
value={selectedElement.gridRows ?? 2}
|
|
3559
|
-
onChange={(e) =>
|
|
3560
|
-
updateElement(selectedElement.id, {
|
|
3561
|
-
gridRows: Math.max(1, Math.min(12, Number(e.target.value) || 2)),
|
|
3562
|
-
})
|
|
3563
|
-
}
|
|
3564
|
-
className="h-7"
|
|
3565
|
-
/>
|
|
3566
|
-
</label>
|
|
3567
|
-
<label className="block space-y-1">
|
|
3568
|
-
<div>{t("panel.config.colCount")}</div>
|
|
3569
|
-
<Input
|
|
3570
|
-
type="number"
|
|
3571
|
-
min={1}
|
|
3572
|
-
max={12}
|
|
3573
|
-
value={selectedElement.gridCols ?? 3}
|
|
3574
|
-
onChange={(e) =>
|
|
3575
|
-
updateElement(selectedElement.id, {
|
|
3576
|
-
gridCols: Math.max(1, Math.min(12, Number(e.target.value) || 3)),
|
|
3577
|
-
})
|
|
3578
|
-
}
|
|
3579
|
-
className="h-7"
|
|
3580
|
-
/>
|
|
3581
|
-
</label>
|
|
3582
|
-
<label className="block space-y-1">
|
|
3583
|
-
<div>{t("panel.config.gapPx")}</div>
|
|
3584
|
-
<Input
|
|
3585
|
-
type="number"
|
|
3586
|
-
min={0}
|
|
3587
|
-
max={80}
|
|
3588
|
-
value={selectedElement.gridGap ?? 8}
|
|
3589
|
-
onChange={(e) =>
|
|
3590
|
-
updateElement(selectedElement.id, {
|
|
3591
|
-
gridGap: Math.max(0, Math.min(80, Number(e.target.value) || 0)),
|
|
3592
|
-
})
|
|
3593
|
-
}
|
|
3594
|
-
className="h-7"
|
|
3595
|
-
/>
|
|
3596
|
-
</label>
|
|
3597
|
-
<label className="block space-y-1">
|
|
3598
|
-
<div>{t("panel.config.paddingPx")}</div>
|
|
3599
|
-
<Input
|
|
3600
|
-
type="number"
|
|
3601
|
-
min={0}
|
|
3602
|
-
max={100}
|
|
3603
|
-
value={selectedElement.gridPadding ?? 10}
|
|
3604
|
-
onChange={(e) =>
|
|
3605
|
-
updateElement(selectedElement.id, {
|
|
3606
|
-
gridPadding: Math.max(0, Math.min(100, Number(e.target.value) || 0)),
|
|
3607
|
-
})
|
|
3608
|
-
}
|
|
3609
|
-
className="h-7"
|
|
3610
|
-
/>
|
|
3611
|
-
</label>
|
|
3612
|
-
<label className="block space-y-1">
|
|
3613
|
-
<div>{t("panel.config.snapThresholdPx")}</div>
|
|
3614
|
-
<Input
|
|
3615
|
-
type="number"
|
|
3616
|
-
min={8}
|
|
3617
|
-
max={120}
|
|
3618
|
-
value={selectedElement.gridSnapThreshold ?? 36}
|
|
3619
|
-
onChange={(e) =>
|
|
3620
|
-
updateElement(selectedElement.id, {
|
|
3621
|
-
gridSnapThreshold: Math.max(
|
|
3622
|
-
8,
|
|
3623
|
-
Math.min(120, Number(e.target.value) || 36)
|
|
3624
|
-
),
|
|
3625
|
-
})
|
|
3626
|
-
}
|
|
3627
|
-
className="h-7"
|
|
3628
|
-
/>
|
|
3629
|
-
</label>
|
|
526
|
+
) : materialType === "scene3d" ? (
|
|
527
|
+
helpers.renderSection(
|
|
528
|
+
"scene3dConfig",
|
|
529
|
+
t("panel.config.sectionScene3d"),
|
|
530
|
+
<PanelConfigScene3dSection
|
|
531
|
+
element={selectedElement}
|
|
532
|
+
updateElement={updateElement}
|
|
533
|
+
/>,
|
|
534
|
+
true,
|
|
535
|
+
[
|
|
536
|
+
t("panel.material.scene3d"),
|
|
537
|
+
"3d",
|
|
538
|
+
"glb",
|
|
539
|
+
t("panel.config.scene3dUploadModels"),
|
|
540
|
+
t("panel.config.groupScene3dCamera"),
|
|
541
|
+
t("panel.config.groupScene3dModelAnim"),
|
|
542
|
+
]
|
|
543
|
+
)
|
|
544
|
+
) : materialType === "grid" ? (
|
|
545
|
+
<PanelConfigGridSection
|
|
546
|
+
element={selectedElement}
|
|
547
|
+
helpers={helpers}
|
|
548
|
+
updateElement={updateElement}
|
|
549
|
+
/>
|
|
550
|
+
) : materialType === "viewport" ? (
|
|
551
|
+
<PanelConfigViewportSection
|
|
552
|
+
element={selectedElement}
|
|
553
|
+
helpers={helpers}
|
|
554
|
+
updateElement={updateElement}
|
|
555
|
+
layers={layers}
|
|
556
|
+
setReferenceCopyMode={setReferenceCopyMode}
|
|
557
|
+
/>
|
|
558
|
+
) : materialType === "reference" ? (
|
|
559
|
+
<PanelConfigReferenceSection
|
|
560
|
+
element={selectedElement}
|
|
561
|
+
helpers={helpers}
|
|
562
|
+
updateElement={updateElement}
|
|
563
|
+
layers={layers}
|
|
564
|
+
setReferenceCopyMode={setReferenceCopyMode}
|
|
565
|
+
/>
|
|
566
|
+
) : materialType === "image" ? null : (
|
|
567
|
+
<div className="text-xs leading-6 text-muted-foreground">
|
|
568
|
+
{t("panel.config.notChartType")}
|
|
3630
569
|
</div>
|
|
3631
570
|
)}
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
<div>{t("panel.config.refLayer")}</div>
|
|
3648
|
-
<Select
|
|
3649
|
-
value={selectedElement.refLayerId ?? "__none__"}
|
|
3650
|
-
onValueChange={(value) =>
|
|
3651
|
-
updateElement(selectedElement.id, {
|
|
3652
|
-
refLayerId: value === "__none__" ? undefined : value,
|
|
3653
|
-
})
|
|
3654
|
-
}
|
|
3655
|
-
>
|
|
3656
|
-
<SelectTrigger className="h-7">
|
|
3657
|
-
<SelectValue placeholder={t("panel.config.pleaseSelectLayer")} />
|
|
3658
|
-
</SelectTrigger>
|
|
3659
|
-
<SelectContent>
|
|
3660
|
-
<SelectItem value="__none__">{t("panel.config.noneNoRef")}</SelectItem>
|
|
3661
|
-
{layers
|
|
3662
|
-
.filter((l) => l.id !== selectedElement.layerId)
|
|
3663
|
-
.map((l) => (
|
|
3664
|
-
<SelectItem key={l.id} value={l.id}>
|
|
3665
|
-
{l.name}
|
|
3666
|
-
</SelectItem>
|
|
3667
|
-
))}
|
|
3668
|
-
</SelectContent>
|
|
3669
|
-
</Select>
|
|
3670
|
-
</label>
|
|
3671
|
-
)}
|
|
3672
|
-
{renderFieldGroup(
|
|
3673
|
-
t("panel.config.groupCopyStrategy"),
|
|
3674
|
-
<>
|
|
3675
|
-
<label className="block space-y-1">
|
|
3676
|
-
<div>{t("panel.config.copyMode")}</div>
|
|
3677
|
-
<Select
|
|
3678
|
-
value={selectedElement.refCopyMode ?? "shallow"}
|
|
3679
|
-
onValueChange={(value) =>
|
|
3680
|
-
setReferenceCopyMode?.(
|
|
3681
|
-
selectedElement.id,
|
|
3682
|
-
value as ReferenceCopyMode
|
|
3683
|
-
)
|
|
3684
|
-
}
|
|
3685
|
-
>
|
|
3686
|
-
<SelectTrigger className="h-7">
|
|
3687
|
-
<SelectValue placeholder={t("panel.config.selectCopyMode")} />
|
|
3688
|
-
</SelectTrigger>
|
|
3689
|
-
<SelectContent>
|
|
3690
|
-
<SelectItem value="shallow">{t("panel.config.shallowFollow")}</SelectItem>
|
|
3691
|
-
<SelectItem value="deep">{t("panel.config.deepFreeze")}</SelectItem>
|
|
3692
|
-
</SelectContent>
|
|
3693
|
-
</Select>
|
|
3694
|
-
</label>
|
|
3695
|
-
</>,
|
|
3696
|
-
<>{t("panel.config.copyStrategyHint")}</>,
|
|
3697
|
-
{ groupKey: "viewportCopyStrategy", defaultOpen: false }
|
|
3698
|
-
)}
|
|
3699
|
-
{renderFieldGroup(
|
|
3700
|
-
t("panel.config.groupViewportOverflow"),
|
|
3701
|
-
<label className="block space-y-1">
|
|
3702
|
-
<div>{t("panel.config.viewportOverflow")}</div>
|
|
3703
|
-
<Select
|
|
3704
|
-
value={normalizeViewportOverflow(selectedElement.viewportOverflow)}
|
|
3705
|
-
onValueChange={(value) =>
|
|
3706
|
-
updateElement(selectedElement.id, {
|
|
3707
|
-
viewportOverflow: value as ViewportOverflowMode,
|
|
3708
|
-
})
|
|
3709
|
-
}
|
|
3710
|
-
>
|
|
3711
|
-
<SelectTrigger className="h-7">
|
|
3712
|
-
<SelectValue placeholder={t("panel.config.viewportOverflow")} />
|
|
3713
|
-
</SelectTrigger>
|
|
3714
|
-
<SelectContent>
|
|
3715
|
-
{VIEWPORT_OVERFLOW_MODES.map((mode) => (
|
|
3716
|
-
<SelectItem key={mode} value={mode}>
|
|
3717
|
-
{mode === "scroll-x"
|
|
3718
|
-
? t("panel.config.viewportOverflowScrollX")
|
|
3719
|
-
: mode === "scroll-y"
|
|
3720
|
-
? t("panel.config.viewportOverflowScrollY")
|
|
3721
|
-
: mode === "scroll"
|
|
3722
|
-
? t("panel.config.viewportOverflowScroll")
|
|
3723
|
-
: t("panel.config.viewportOverflowClip")}
|
|
3724
|
-
</SelectItem>
|
|
3725
|
-
))}
|
|
3726
|
-
</SelectContent>
|
|
3727
|
-
</Select>
|
|
3728
|
-
</label>
|
|
3729
|
-
)}
|
|
3730
|
-
</>,
|
|
3731
|
-
true,
|
|
3732
|
-
[
|
|
3733
|
-
t("panel.config.searchKwViewport"),
|
|
3734
|
-
"viewport",
|
|
3735
|
-
t("panel.config.refLayer"),
|
|
3736
|
-
t("panel.config.viewportOverflow"),
|
|
3737
|
-
t("panel.config.viewportOverflowScrollX"),
|
|
3738
|
-
t("panel.config.viewportOverflowClip"),
|
|
3739
|
-
t("panel.material.shallowCopy"),
|
|
3740
|
-
t("panel.material.deepCopy"),
|
|
3741
|
-
],
|
|
3742
|
-
<>{t("panel.config.viewportHint")}</>
|
|
3743
|
-
)
|
|
3744
|
-
) : selectedElement.materialType === "reference" ? (
|
|
3745
|
-
renderSection(
|
|
3746
|
-
"reference",
|
|
3747
|
-
t("panel.config.sectionReference"),
|
|
3748
|
-
<>
|
|
3749
|
-
{renderFieldGroup(
|
|
3750
|
-
t("panel.config.groupRefSource"),
|
|
3751
|
-
<label className="block space-y-1">
|
|
3752
|
-
<div>{t("panel.config.refLayer")}</div>
|
|
3753
|
-
<Select
|
|
3754
|
-
value={selectedElement.refLayerId ?? "__none__"}
|
|
3755
|
-
onValueChange={(value) =>
|
|
3756
|
-
updateElement(selectedElement.id, {
|
|
3757
|
-
refLayerId: value === "__none__" ? undefined : value,
|
|
3758
|
-
})
|
|
3759
|
-
}
|
|
3760
|
-
>
|
|
3761
|
-
<SelectTrigger className="h-7">
|
|
3762
|
-
<SelectValue placeholder={t("panel.config.pleaseSelectLayer")} />
|
|
3763
|
-
</SelectTrigger>
|
|
3764
|
-
<SelectContent>
|
|
3765
|
-
<SelectItem value="__none__">{t("panel.config.noneNoRef")}</SelectItem>
|
|
3766
|
-
{layers
|
|
3767
|
-
.filter((l) => l.id !== selectedElement.layerId)
|
|
3768
|
-
.map((l) => (
|
|
3769
|
-
<SelectItem key={l.id} value={l.id}>
|
|
3770
|
-
{l.name}
|
|
3771
|
-
</SelectItem>
|
|
3772
|
-
))}
|
|
3773
|
-
</SelectContent>
|
|
3774
|
-
</Select>
|
|
3775
|
-
</label>
|
|
3776
|
-
)}
|
|
3777
|
-
{renderFieldGroup(
|
|
3778
|
-
t("panel.config.groupCopyStrategy"),
|
|
3779
|
-
<>
|
|
3780
|
-
<label className="block space-y-1">
|
|
3781
|
-
<div>{t("panel.config.copyMode")}</div>
|
|
3782
|
-
<Select
|
|
3783
|
-
value={selectedElement.refCopyMode ?? "shallow"}
|
|
3784
|
-
onValueChange={(value) =>
|
|
3785
|
-
setReferenceCopyMode?.(
|
|
3786
|
-
selectedElement.id,
|
|
3787
|
-
value as ReferenceCopyMode
|
|
3788
|
-
)
|
|
3789
|
-
}
|
|
3790
|
-
>
|
|
3791
|
-
<SelectTrigger className="h-7">
|
|
3792
|
-
<SelectValue placeholder={t("panel.config.selectCopyMode")} />
|
|
3793
|
-
</SelectTrigger>
|
|
3794
|
-
<SelectContent>
|
|
3795
|
-
<SelectItem value="shallow">{t("panel.config.shallowFollow")}</SelectItem>
|
|
3796
|
-
<SelectItem value="deep">{t("panel.config.deepFreeze")}</SelectItem>
|
|
3797
|
-
</SelectContent>
|
|
3798
|
-
</Select>
|
|
3799
|
-
</label>
|
|
3800
|
-
</>,
|
|
3801
|
-
<>
|
|
3802
|
-
{t("panel.config.copyStrategyHint")}
|
|
3803
|
-
</>,
|
|
3804
|
-
{ groupKey: "referenceCopyStrategy", defaultOpen: false }
|
|
3805
|
-
)}
|
|
3806
|
-
</>,
|
|
3807
|
-
true,
|
|
3808
|
-
[t("panel.config.searchKwRef"), "ref", t("panel.config.layer"), t("panel.material.shallowCopy"), t("panel.material.deepCopy"), "snapshot"]
|
|
3809
|
-
)
|
|
3810
|
-
) : selectedElement.materialType === "image" ? null : (
|
|
3811
|
-
<div className="text-xs leading-6 text-muted-foreground">
|
|
3812
|
-
{t("panel.config.notChartType")}
|
|
3813
|
-
</div>
|
|
3814
|
-
)}
|
|
3815
|
-
{renderSection(
|
|
3816
|
-
"styleBackground",
|
|
3817
|
-
t("panel.config.sectionStyleBackground"),
|
|
3818
|
-
<>
|
|
3819
|
-
{renderFieldGroup(
|
|
3820
|
-
t("panel.config.groupBgFill"),
|
|
3821
|
-
<>
|
|
3822
|
-
{renderColorField(
|
|
3823
|
-
t("panel.config.backgroundColor"),
|
|
3824
|
-
selectedElement.style?.backgroundColor ?? "",
|
|
3825
|
-
(next) => updateSelectedStyle({ backgroundColor: next || undefined })
|
|
3826
|
-
)}
|
|
3827
|
-
</>
|
|
3828
|
-
)}
|
|
3829
|
-
{renderFieldGroup(
|
|
3830
|
-
t("panel.config.groupBgLayout"),
|
|
3831
|
-
<>
|
|
3832
|
-
<label className="block space-y-1">
|
|
3833
|
-
<div>{t("panel.config.backgroundImage")}</div>
|
|
3834
|
-
<Input
|
|
3835
|
-
value={selectedElement.style?.backgroundImage ?? ""}
|
|
3836
|
-
onChange={(e) => updateSelectedStyle({ backgroundImage: e.target.value || undefined })}
|
|
3837
|
-
placeholder='url("https://...") / linear-gradient(...)'
|
|
3838
|
-
className="h-7"
|
|
3839
|
-
/>
|
|
3840
|
-
</label>
|
|
3841
|
-
<div className="flex items-center gap-2">
|
|
3842
|
-
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
3843
|
-
{t("panel.config.uploadImage")}
|
|
3844
|
-
<Input
|
|
3845
|
-
type="file"
|
|
3846
|
-
accept="image/*"
|
|
3847
|
-
className="hidden"
|
|
3848
|
-
onChange={async (e) => {
|
|
3849
|
-
const file = e.target.files?.[0];
|
|
3850
|
-
e.currentTarget.value = "";
|
|
3851
|
-
if (!file) return;
|
|
3852
|
-
await handleUploadBackgroundImage(file);
|
|
3853
|
-
}}
|
|
3854
|
-
/>
|
|
3855
|
-
</label>
|
|
3856
|
-
{uploadStatus ? (
|
|
3857
|
-
<span className="text-[11px] text-muted-foreground">{uploadStatus}</span>
|
|
3858
|
-
) : null}
|
|
3859
|
-
</div>
|
|
3860
|
-
<div className="grid grid-cols-2 gap-2">
|
|
3861
|
-
<label className="block space-y-1">
|
|
3862
|
-
<div>{t("panel.config.backgroundSize")}</div>
|
|
3863
|
-
<Select
|
|
3864
|
-
value={selectedElement.style?.backgroundSize ?? "__none__"}
|
|
3865
|
-
onValueChange={(value) =>
|
|
3866
|
-
updateSelectedStyle({
|
|
3867
|
-
backgroundSize: value === "__none__" ? undefined : value,
|
|
3868
|
-
})
|
|
3869
|
-
}
|
|
3870
|
-
>
|
|
3871
|
-
<SelectTrigger className="h-7">
|
|
3872
|
-
<SelectValue placeholder={t("panel.config.selectBackgroundSize")} />
|
|
3873
|
-
</SelectTrigger>
|
|
3874
|
-
<SelectContent>
|
|
3875
|
-
<SelectItem value="__none__">{t("common.default")}</SelectItem>
|
|
3876
|
-
<SelectItem value="cover">cover</SelectItem>
|
|
3877
|
-
<SelectItem value="contain">contain</SelectItem>
|
|
3878
|
-
<SelectItem value="100% 100%">100% 100%</SelectItem>
|
|
3879
|
-
<SelectItem value="auto">auto</SelectItem>
|
|
3880
|
-
</SelectContent>
|
|
3881
|
-
</Select>
|
|
3882
|
-
</label>
|
|
3883
|
-
<label className="block space-y-1">
|
|
3884
|
-
<div>{t("panel.config.backgroundPosition")}</div>
|
|
3885
|
-
<Select
|
|
3886
|
-
value={selectedElement.style?.backgroundPosition ?? "__none__"}
|
|
3887
|
-
onValueChange={(value) =>
|
|
3888
|
-
updateSelectedStyle({
|
|
3889
|
-
backgroundPosition: value === "__none__" ? undefined : value,
|
|
3890
|
-
})
|
|
3891
|
-
}
|
|
3892
|
-
>
|
|
3893
|
-
<SelectTrigger className="h-7">
|
|
3894
|
-
<SelectValue placeholder={t("panel.config.selectBackgroundPosition")} />
|
|
3895
|
-
</SelectTrigger>
|
|
3896
|
-
<SelectContent>
|
|
3897
|
-
<SelectItem value="__none__">{t("common.default")}</SelectItem>
|
|
3898
|
-
<SelectItem value="center">center</SelectItem>
|
|
3899
|
-
<SelectItem value="top">top</SelectItem>
|
|
3900
|
-
<SelectItem value="bottom">bottom</SelectItem>
|
|
3901
|
-
<SelectItem value="left">left</SelectItem>
|
|
3902
|
-
<SelectItem value="right">right</SelectItem>
|
|
3903
|
-
<SelectItem value="top left">top left</SelectItem>
|
|
3904
|
-
<SelectItem value="top right">top right</SelectItem>
|
|
3905
|
-
<SelectItem value="bottom left">bottom left</SelectItem>
|
|
3906
|
-
<SelectItem value="bottom right">bottom right</SelectItem>
|
|
3907
|
-
</SelectContent>
|
|
3908
|
-
</Select>
|
|
3909
|
-
</label>
|
|
3910
|
-
</div>
|
|
3911
|
-
</>,
|
|
3912
|
-
undefined,
|
|
3913
|
-
{ groupKey: "styleBgLayout", defaultOpen: false }
|
|
3914
|
-
)}
|
|
3915
|
-
</>,
|
|
3916
|
-
false,
|
|
3917
|
-
[t("panel.config.backgroundColor"), t("panel.config.backgroundImage"), "background", "backgroundSize", "backgroundPosition", t("panel.config.searchKwLayout")]
|
|
3918
|
-
)}
|
|
3919
|
-
|
|
3920
|
-
{renderSection(
|
|
3921
|
-
"styleBorder",
|
|
3922
|
-
t("panel.config.sectionStyleBorder"),
|
|
3923
|
-
<>
|
|
3924
|
-
{renderFieldGroup(
|
|
3925
|
-
t("panel.config.groupBorderGeometry"),
|
|
3926
|
-
<div className="grid grid-cols-2 gap-2">
|
|
3927
|
-
<label className="block space-y-1">
|
|
3928
|
-
<div>{t("panel.config.borderWidth")}</div>
|
|
3929
|
-
<Input
|
|
3930
|
-
type="number"
|
|
3931
|
-
min={0}
|
|
3932
|
-
value={selectedElement.style?.borderWidth ?? 0}
|
|
3933
|
-
onChange={(e) =>
|
|
3934
|
-
updateSelectedStyle({ borderWidth: Math.max(0, Number(e.target.value) || 0) })
|
|
3935
|
-
}
|
|
3936
|
-
className="h-7"
|
|
3937
|
-
/>
|
|
3938
|
-
</label>
|
|
3939
|
-
<label className="block space-y-1">
|
|
3940
|
-
<div>{t("panel.config.borderRadius")}</div>
|
|
3941
|
-
<Input
|
|
3942
|
-
type="number"
|
|
3943
|
-
min={0}
|
|
3944
|
-
value={selectedElement.style?.borderRadius ?? 0}
|
|
3945
|
-
onChange={(e) =>
|
|
3946
|
-
updateSelectedStyle({ borderRadius: Math.max(0, Number(e.target.value) || 0) })
|
|
3947
|
-
}
|
|
3948
|
-
className="h-7"
|
|
3949
|
-
/>
|
|
3950
|
-
</label>
|
|
3951
|
-
</div>
|
|
3952
|
-
)}
|
|
3953
|
-
{renderFieldGroup(
|
|
3954
|
-
t("panel.config.groupBorderVisual"),
|
|
3955
|
-
<div className="grid grid-cols-2 gap-2">
|
|
3956
|
-
<label className="block space-y-1">
|
|
3957
|
-
<div>{t("panel.config.borderStyle")}</div>
|
|
3958
|
-
<Select
|
|
3959
|
-
value={selectedElement.style?.borderStyle ?? "solid"}
|
|
3960
|
-
onValueChange={(value) =>
|
|
3961
|
-
updateSelectedStyle({
|
|
3962
|
-
borderStyle: value as NonNullable<PanelElementStyle["borderStyle"]>,
|
|
3963
|
-
})
|
|
3964
|
-
}
|
|
3965
|
-
>
|
|
3966
|
-
<SelectTrigger className="h-7">
|
|
3967
|
-
<SelectValue placeholder={t("panel.config.selectBorderStyle")} />
|
|
3968
|
-
</SelectTrigger>
|
|
3969
|
-
<SelectContent>
|
|
3970
|
-
<SelectItem value="none">none</SelectItem>
|
|
3971
|
-
<SelectItem value="solid">solid</SelectItem>
|
|
3972
|
-
<SelectItem value="dashed">dashed</SelectItem>
|
|
3973
|
-
<SelectItem value="dotted">dotted</SelectItem>
|
|
3974
|
-
<SelectItem value="double">double</SelectItem>
|
|
3975
|
-
</SelectContent>
|
|
3976
|
-
</Select>
|
|
3977
|
-
</label>
|
|
3978
|
-
{renderColorField(
|
|
3979
|
-
t("panel.config.borderColor"),
|
|
3980
|
-
selectedElement.style?.borderColor ?? "",
|
|
3981
|
-
(next) => updateSelectedStyle({ borderColor: next || undefined })
|
|
3982
|
-
)}
|
|
3983
|
-
</div>
|
|
3984
|
-
)}
|
|
3985
|
-
</>,
|
|
3986
|
-
false,
|
|
3987
|
-
[t("panel.config.searchKwBorder"), "border", t("panel.config.searchKwWidth"), t("panel.config.searchKwRadius"), t("panel.config.color"), t("panel.config.searchKwStyle")]
|
|
3988
|
-
)}
|
|
3989
|
-
|
|
3990
|
-
{hasSearch && renderedSectionCount === 0 ? (
|
|
3991
|
-
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3992
|
-
{t("panel.config.noMatch")}
|
|
3993
|
-
</div>
|
|
3994
|
-
) : null}
|
|
3995
|
-
</div>
|
|
3996
|
-
</fieldset>
|
|
3997
|
-
</div>
|
|
3998
|
-
) : null}
|
|
3999
|
-
</aside>
|
|
571
|
+
<PanelConfigStyleSections
|
|
572
|
+
element={selectedElement}
|
|
573
|
+
helpers={helpers}
|
|
574
|
+
updateElement={updateElement}
|
|
575
|
+
/>
|
|
576
|
+
<ConfigSearchEmpty
|
|
577
|
+
hasSearch={hasSearch}
|
|
578
|
+
matchCountRef={matchCountRef}
|
|
579
|
+
label={t("panel.config.noMatch")}
|
|
580
|
+
/>
|
|
581
|
+
</div>
|
|
582
|
+
</fieldset>
|
|
583
|
+
</div>
|
|
584
|
+
) : null}
|
|
585
|
+
</aside>
|
|
4000
586
|
</ScopeConfigProvider>
|
|
4001
587
|
);
|
|
4002
588
|
}
|
|
4003
|
-
|