@arronqzy/react-view 1.0.3 → 1.0.7

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.
Files changed (43) hide show
  1. package/dist/styles.css +555 -15
  2. package/package.json +14 -7
  3. package/readme.md +74 -5
  4. package/src/core/template.tsx +12 -2
  5. package/src/panel/ReactViewOnlinePreview.tsx +18 -8
  6. package/src/panel/ReactViewPanel.tsx +403 -177
  7. package/src/panel/ai/AssistantChatPanel.tsx +895 -0
  8. package/src/panel/ai/AssistantMarkdown.tsx +256 -0
  9. package/src/panel/ai/applyAssistantAction.ts +652 -0
  10. package/src/panel/ai/revealConfigField.ts +137 -0
  11. package/src/panel/components/ConfigHintIcon.tsx +50 -0
  12. package/src/panel/components/ElementsLayer.tsx +39 -22
  13. package/src/panel/components/MaterialSidebar.tsx +157 -109
  14. package/src/panel/components/MoveableLayer.tsx +75 -61
  15. package/src/panel/components/PanelCanvas.tsx +81 -64
  16. package/src/panel/components/PanelConfigSidebar.tsx +471 -448
  17. package/src/panel/components/SelectLayer.tsx +1 -0
  18. package/src/panel/components/ViewElementScopePanel.tsx +13 -12
  19. package/src/panel/components/WorkspaceConfigSidebar.tsx +5 -2
  20. package/src/panel/components/WorkspaceProjectNav.tsx +25 -14
  21. package/src/panel/components/WorkspaceStageSplit.tsx +3 -1
  22. package/src/panel/components/scope-config/ScopeConfigContext.tsx +9 -0
  23. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +242 -0
  24. package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +12 -0
  25. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +6 -3
  26. package/src/panel/components/table/ConditionEditor.tsx +205 -0
  27. package/src/panel/components/table/JsonCodeEditor.tsx +115 -0
  28. package/src/panel/components/table/PanelConfigTableSection.tsx +2192 -0
  29. package/src/panel/components/table/TableNodeContent.tsx +544 -0
  30. package/src/panel/constants/messages.ts +76 -34
  31. package/src/panel/hooks/usePanelElements.ts +64 -52
  32. package/src/panel/hooks/useRafThrottledScroll.ts +30 -0
  33. package/src/panel/hooks/useWorkspaceProjects.ts +11 -8
  34. package/src/panel/types.ts +6 -0
  35. package/src/panel/utils/chartOptionBuilder.ts +12 -1
  36. package/src/panel/utils/panelElementDefaults.ts +69 -39
  37. package/src/panel/utils/scope-field-labels.ts +51 -37
  38. package/src/panel/utils/scope-template-chart.ts +92 -0
  39. package/src/panel/utils/scope-template-preview.ts +128 -0
  40. package/src/panel/utils/scope-template-spread.ts +252 -0
  41. package/src/panel/utils/scope-template-warnings.ts +131 -11
  42. package/src/panel/utils/scope-template.ts +105 -3
  43. package/src/tailwind.css +36 -15
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
  import {
3
4
  Card,
4
5
  CardContent,
@@ -19,10 +20,6 @@ import {
19
20
  SelectTrigger,
20
21
  SelectValue,
21
22
  Textarea,
22
- Tooltip,
23
- TooltipContent,
24
- TooltipProvider,
25
- TooltipTrigger,
26
23
  } from "@arronqzy/ui";
27
24
  import type {
28
25
  PanelChartConfig,
@@ -30,14 +27,20 @@ import type {
30
27
  PanelElementStyle,
31
28
  ReferenceCopyMode,
32
29
  } from "../types";
33
- import { buildChartOption, CHART_TYPES, getChartValuesDisplayText } from "../utils/chartOptionBuilder";
30
+ import { buildChartOption, CHART_TYPES, getChartLabelsDisplayText, getChartValuesDisplayText } from "../utils/chartOptionBuilder";
34
31
  import type { PanelLayer } from "../types";
35
- import { PANEL_MESSAGES } from "../constants/messages";
32
+ import { getPanelMessages } from "../constants/messages";
36
33
  import { ViewElementScopePanel } from "./ViewElementScopePanel";
34
+ import { ConfigHintIcon } from "./ConfigHintIcon";
37
35
  import { hasViewElementScope } from "../scope/view-scope-store";
38
36
  import { collectElementScopeWarnings } from "../utils/scope-template-warnings";
39
37
  import { ScopeConfigProvider } from "./scope-config/ScopeConfigContext";
40
38
  import { ScopeTemplateWarningsPanel } from "./scope-config/ScopeTemplateWarningsPanel";
39
+ import { PanelConfigTableSection } from "./table/PanelConfigTableSection";
40
+ import {
41
+ highlightConfigField,
42
+ subscribeRevealPanelConfig,
43
+ } from "../ai/revealConfigField";
41
44
 
42
45
  type UpdateElement = (
43
46
  id: string,
@@ -59,6 +62,7 @@ export type PanelConfigSidebarProps = {
59
62
  ) => void;
60
63
  /** 蓝图视图绑定节点写入的信号 scope,用于展示与模版解析 */
61
64
  viewElementScope?: unknown;
65
+ blueprintNodeOptions?: { id: string; label: string }[];
62
66
  };
63
67
 
64
68
  export function PanelConfigSidebar({
@@ -71,7 +75,11 @@ export function PanelConfigSidebar({
71
75
  onExcludeSelectedNode,
72
76
  onAdjustNodeZOrder,
73
77
  viewElementScope,
78
+ blueprintNodeOptions = [],
74
79
  }: PanelConfigSidebarProps) {
80
+ const { t } = useI18n();
81
+ const messages = React.useMemo(() => getPanelMessages(t), [t]);
82
+
75
83
  const [isAdvancedOptionMode, setIsAdvancedOptionMode] = useState(false);
76
84
  const [optionJsonText, setOptionJsonText] = useState("{}");
77
85
  const [optionJsonError, setOptionJsonError] = useState<string | null>(null);
@@ -116,9 +124,9 @@ export function PanelConfigSidebar({
116
124
  const readonlyReason = !selectedElement
117
125
  ? ""
118
126
  : selectedElement.locked
119
- ? PANEL_MESSAGES.nodeConfigLocked
127
+ ? messages.nodeConfigLocked
120
128
  : selectedLayer?.locked
121
- ? PANEL_MESSAGES.nodeConfigLayerLocked
129
+ ? messages.nodeConfigLayerLocked
122
130
  : "";
123
131
  const selectedChartType = (selectedElement?.materialType ?? "") as
124
132
  | "bar"
@@ -138,7 +146,7 @@ export function PanelConfigSidebar({
138
146
  hasViewElementScope(selectedElement.id);
139
147
  const scopeWarnings = useMemo(() => {
140
148
  if (!selectedElement || viewElementScope === undefined) return [];
141
- return collectElementScopeWarnings(selectedElement, viewElementScope);
149
+ return collectElementScopeWarnings(selectedElement, viewElementScope, t);
142
150
  }, [selectedElement, viewElementScope]);
143
151
  const effectiveSelectedElements = useMemo(() => {
144
152
  if (selectedElements.length > 0) return selectedElements;
@@ -171,7 +179,8 @@ export function PanelConfigSidebar({
171
179
 
172
180
  useEffect(() => {
173
181
  if (!selectedElement || selectedElement.materialType !== "text") return;
174
- const nextHtml = selectedElement.textHtml ?? "<p>双击输入文本</p>";
182
+ const nextHtml =
183
+ selectedElement.textHtml ?? `<p>${t("panel.defaults.doubleClickTextHtml")}</p>`;
175
184
  if (textEditorRef.current && textEditorRef.current.innerHTML !== nextHtml) {
176
185
  textEditorRef.current.innerHTML = nextHtml;
177
186
  }
@@ -256,13 +265,13 @@ export function PanelConfigSidebar({
256
265
  const base64 = await new Promise<string>((resolve, reject) => {
257
266
  const reader = new FileReader();
258
267
  reader.onload = () => resolve(String(reader.result ?? ""));
259
- reader.onerror = () => reject(new Error(PANEL_MESSAGES.readImageFailed));
268
+ reader.onerror = () => reject(new Error(messages.readImageFailed));
260
269
  reader.readAsDataURL(file);
261
270
  });
262
271
  updateSelectedStyle({
263
272
  backgroundImage: `url("${base64}")`,
264
273
  });
265
- setUploadStatus("已写入 base64");
274
+ setUploadStatus(t("panel.config.uploadWrittenBase64"));
266
275
  try {
267
276
  const form = new FormData();
268
277
  form.append("file", file);
@@ -271,10 +280,10 @@ export function PanelConfigSidebar({
271
280
  const data = (await resp.json()) as { url?: string };
272
281
  if (data.url) {
273
282
  updateSelectedStyle({ backgroundImageRemoteUrl: data.url });
274
- setUploadStatus("已上传服务器并写入 base64");
283
+ setUploadStatus(t("panel.config.uploadServerAndBase64"));
275
284
  }
276
285
  } catch {
277
- setUploadStatus("服务器上传失败,仅保留 base64");
286
+ setUploadStatus(t("panel.config.uploadServerFailedKeepBase64"));
278
287
  }
279
288
  },
280
289
  [selectedElement, updateSelectedStyle]
@@ -292,11 +301,11 @@ export function PanelConfigSidebar({
292
301
  const base64 = await new Promise<string>((resolve, reject) => {
293
302
  const reader = new FileReader();
294
303
  reader.onload = () => resolve(String(reader.result ?? ""));
295
- reader.onerror = () => reject(new Error(PANEL_MESSAGES.readAudioFailed));
304
+ reader.onerror = () => reject(new Error(messages.readAudioFailed));
296
305
  reader.readAsDataURL(file);
297
306
  });
298
307
  updateSelectedAudio({ audioSrc: base64 });
299
- setAudioStatus(PANEL_MESSAGES.audioLocalSaved);
308
+ setAudioStatus(messages.audioLocalSaved);
300
309
  try {
301
310
  const form = new FormData();
302
311
  form.append("file", file);
@@ -305,10 +314,10 @@ export function PanelConfigSidebar({
305
314
  const data = (await resp.json()) as { url?: string };
306
315
  if (data.url) {
307
316
  updateSelectedAudio({ audioRemoteUrl: data.url });
308
- setAudioStatus(PANEL_MESSAGES.audioRemoteUploaded);
317
+ setAudioStatus(messages.audioRemoteUploaded);
309
318
  }
310
319
  } catch {
311
- setAudioStatus(PANEL_MESSAGES.audioServerUploadFailed);
320
+ setAudioStatus(messages.audioServerUploadFailed);
312
321
  }
313
322
  },
314
323
  [selectedElement, updateSelectedAudio]
@@ -319,11 +328,11 @@ export function PanelConfigSidebar({
319
328
  const base64 = await new Promise<string>((resolve, reject) => {
320
329
  const reader = new FileReader();
321
330
  reader.onload = () => resolve(String(reader.result ?? ""));
322
- reader.onerror = () => reject(new Error(PANEL_MESSAGES.readImageFailed));
331
+ reader.onerror = () => reject(new Error(messages.readImageFailed));
323
332
  reader.readAsDataURL(file);
324
333
  });
325
334
  updateSelectedAudio({ audioPosterImage: base64 });
326
- setAudioStatus(PANEL_MESSAGES.audioPosterSet);
335
+ setAudioStatus(messages.audioPosterSet);
327
336
  },
328
337
  [selectedElement, updateSelectedAudio]
329
338
  );
@@ -333,7 +342,7 @@ export function PanelConfigSidebar({
333
342
  const startRecordingAudio = useCallback(async () => {
334
343
  if (!selectedElement || selectedElement.materialType !== "audio") return;
335
344
  if (!navigator.mediaDevices?.getUserMedia) {
336
- setAudioStatus(PANEL_MESSAGES.audioRecordUnsupported);
345
+ setAudioStatus(messages.audioRecordUnsupported);
337
346
  return;
338
347
  }
339
348
  try {
@@ -350,11 +359,11 @@ export function PanelConfigSidebar({
350
359
  const dataUrl = await new Promise<string>((resolve, reject) => {
351
360
  const reader = new FileReader();
352
361
  reader.onload = () => resolve(String(reader.result ?? ""));
353
- reader.onerror = () => reject(new Error(PANEL_MESSAGES.readRecordAudioFailed));
362
+ reader.onerror = () => reject(new Error(messages.readRecordAudioFailed));
354
363
  reader.readAsDataURL(blob);
355
364
  });
356
365
  updateSelectedAudio({ audioSrc: dataUrl });
357
- setAudioStatus(PANEL_MESSAGES.audioRecordSaved);
366
+ setAudioStatus(messages.audioRecordSaved);
358
367
  recordStreamRef.current?.getTracks().forEach((track) => track.stop());
359
368
  recordStreamRef.current = null;
360
369
  recorderRef.current = null;
@@ -362,9 +371,9 @@ export function PanelConfigSidebar({
362
371
  };
363
372
  recorder.start();
364
373
  setIsRecordingAudio(true);
365
- setAudioStatus(PANEL_MESSAGES.audioRecording);
374
+ setAudioStatus(messages.audioRecording);
366
375
  } catch {
367
- setAudioStatus(PANEL_MESSAGES.audioRecordStartFailed);
376
+ setAudioStatus(messages.audioRecordStartFailed);
368
377
  setIsRecordingAudio(false);
369
378
  }
370
379
  }, [selectedElement, updateSelectedAudio]);
@@ -381,11 +390,11 @@ export function PanelConfigSidebar({
381
390
  const base64 = await new Promise<string>((resolve, reject) => {
382
391
  const reader = new FileReader();
383
392
  reader.onload = () => resolve(String(reader.result ?? ""));
384
- reader.onerror = () => reject(new Error(PANEL_MESSAGES.readVideoFailed));
393
+ reader.onerror = () => reject(new Error(messages.readVideoFailed));
385
394
  reader.readAsDataURL(file);
386
395
  });
387
396
  updateSelectedVideo({ videoSrc: base64 });
388
- setVideoStatus(PANEL_MESSAGES.videoLocalSaved);
397
+ setVideoStatus(messages.videoLocalSaved);
389
398
  try {
390
399
  const form = new FormData();
391
400
  form.append("file", file);
@@ -394,10 +403,10 @@ export function PanelConfigSidebar({
394
403
  const data = (await resp.json()) as { url?: string };
395
404
  if (data.url) {
396
405
  updateSelectedVideo({ videoRemoteUrl: data.url });
397
- setVideoStatus(PANEL_MESSAGES.videoRemoteUploaded);
406
+ setVideoStatus(messages.videoRemoteUploaded);
398
407
  }
399
408
  } catch {
400
- setVideoStatus(PANEL_MESSAGES.videoServerUploadFailed);
409
+ setVideoStatus(messages.videoServerUploadFailed);
401
410
  }
402
411
  },
403
412
  [selectedElement, updateSelectedVideo]
@@ -410,12 +419,25 @@ export function PanelConfigSidebar({
410
419
  setExpandedSections((prev) => ({ ...prev, [key]: next }));
411
420
  };
412
421
 
422
+ useEffect(() => {
423
+ return subscribeRevealPanelConfig((detail) => {
424
+ for (const section of detail.sections) {
425
+ setSectionExpanded(section, true);
426
+ }
427
+ window.requestAnimationFrame(() => {
428
+ const field = detail.fields[0];
429
+ if (field) highlightConfigField(field);
430
+ });
431
+ });
432
+ }, []);
433
+
413
434
  const renderSection = (
414
435
  key: string,
415
436
  title: string,
416
437
  children: React.ReactNode,
417
438
  defaultOpen = true,
418
- searchTerms: string[] = []
439
+ searchTerms: string[] = [],
440
+ hint?: React.ReactNode
419
441
  ) => {
420
442
  const hit =
421
443
  !hasSearch ||
@@ -427,6 +449,7 @@ export function PanelConfigSidebar({
427
449
  open={hasSearch ? true : isSectionExpanded(key, defaultOpen)}
428
450
  onOpenChange={(open) => setSectionExpanded(key, open)}
429
451
  className="rounded-xl border border-border/70 bg-card/95 shadow-sm"
452
+ data-config-section={key}
430
453
  >
431
454
  <div className="flex items-center gap-1.5 px-3 py-2">
432
455
  <CollapsibleTrigger asChild>
@@ -437,7 +460,10 @@ export function PanelConfigSidebar({
437
460
  {isSectionExpanded(key, defaultOpen) ? "▾" : "▸"}
438
461
  </button>
439
462
  </CollapsibleTrigger>
440
- <div className="text-[11px] font-semibold tracking-wide text-muted-foreground">{title}</div>
463
+ <div className="flex min-w-0 flex-1 items-center gap-1">
464
+ <div className="text-[11px] font-semibold tracking-wide text-muted-foreground">{title}</div>
465
+ {hint ? <ConfigHintIcon label={title}>{hint}</ConfigHintIcon> : null}
466
+ </div>
441
467
  </div>
442
468
  <CollapsibleContent className="space-y-3 border-t border-border/60 bg-muted/[0.1] px-3 pb-3 pt-2.5">
443
469
  {children}
@@ -446,9 +472,16 @@ export function PanelConfigSidebar({
446
472
  );
447
473
  };
448
474
 
449
- const renderFieldGroup = (title: string, children: React.ReactNode) => (
475
+ const renderFieldGroup = (
476
+ title: string,
477
+ children: React.ReactNode,
478
+ hint?: React.ReactNode
479
+ ) => (
450
480
  <div className="space-y-2.5 rounded-lg border border-border/55 bg-background/80 p-2.5">
451
- <div className="text-[11px] font-semibold text-muted-foreground">{title}</div>
481
+ <div className="flex items-center gap-1">
482
+ <div className="text-[11px] font-semibold text-muted-foreground">{title}</div>
483
+ {hint ? <ConfigHintIcon label={title}>{hint}</ConfigHintIcon> : null}
484
+ </div>
452
485
  {children}
453
486
  </div>
454
487
  );
@@ -479,7 +512,7 @@ export function PanelConfigSidebar({
479
512
  value={/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value || "") ? value : "#000000"}
480
513
  onChange={(e) => onTextChange(e.target.value)}
481
514
  className="h-7 w-10 cursor-pointer p-1"
482
- aria-label={`${label}调色盘`}
515
+ aria-label={t("common.colorPickerAria", { label })}
483
516
  />
484
517
  </div>
485
518
  </label>
@@ -494,50 +527,24 @@ export function PanelConfigSidebar({
494
527
  const renderOptionLabel = (label: string, keyPath: string, desc: string) => (
495
528
  <div className="flex min-w-0 items-center gap-1">
496
529
  <span className={optionLabelTextClass} title={`${label}(${keyPath})`}>{label}</span>
497
- <TooltipProvider delayDuration={120}>
498
- <Tooltip>
499
- <TooltipTrigger asChild>
500
- <button
501
- type="button"
502
- className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] text-muted-foreground"
503
- aria-label={`${label}说明`}
504
- >
505
- ?
506
- </button>
507
- </TooltipTrigger>
508
- <TooltipContent className="max-w-[260px] text-[11px]">
509
- <div>Key: {keyPath}</div>
510
- <div>{desc}</div>
511
- </TooltipContent>
512
- </Tooltip>
513
- </TooltipProvider>
530
+ <ConfigHintIcon label={label}>
531
+ <div>Key: {keyPath}</div>
532
+ <div>{desc}</div>
533
+ </ConfigHintIcon>
514
534
  </div>
515
535
  );
516
- const renderFormatterLabel = (label = "Tooltip Formatter(可选)") => (
536
+ const renderFormatterLabel = (label = t("panel.config.tooltipFormatter")) => (
517
537
  <div className="flex min-w-0 items-center gap-1">
518
538
  <span className={optionLabelTextClass} title={label}>{label}</span>
519
- <TooltipProvider delayDuration={120}>
520
- <Tooltip>
521
- <TooltipTrigger asChild>
522
- <button
523
- type="button"
524
- className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] text-muted-foreground"
525
- aria-label="Tooltip Formatter 说明"
526
- >
527
- ?
528
- </button>
529
- </TooltipTrigger>
530
- <TooltipContent className="max-w-[360px] text-[11px] leading-5">
531
- <div className="font-medium">可用占位符</div>
532
- <div>{"{a}=系列名, {b}=类目名, {c}=数值, {d}=百分比(饼图)"}</div>
533
- <div className="mt-1 font-medium">常用模板 + 输出示例</div>
534
- <div>{"1) {b}: {c} -> 周一: 120"}</div>
535
- <div>{"2) {a}<br/>{b}: {c} -> 销量<br/>周一: 120"}</div>
536
- <div>{"3) {b}: {c} ({d}%) -> 访问来源: 335 (42%)"}</div>
537
- <div>{"4) ¥{c} / {b} -> ¥120 / 周一"}</div>
538
- </TooltipContent>
539
- </Tooltip>
540
- </TooltipProvider>
539
+ <ConfigHintIcon label="Tooltip Formatter" contentClassName="max-w-[360px]">
540
+ <div className="font-medium">{t("panel.config.formatterPlaceholdersTitle")}</div>
541
+ <div>{t("panel.config.formatterPlaceholders")}</div>
542
+ <div className="mt-1 font-medium">{t("panel.config.formatterExamplesTitle")}</div>
543
+ <div>{t("panel.config.formatterExample1")}</div>
544
+ <div>{t("panel.config.formatterExample2")}</div>
545
+ <div>{t("panel.config.formatterExample3")}</div>
546
+ <div>{t("panel.config.formatterExample4")}</div>
547
+ </ConfigHintIcon>
541
548
  </div>
542
549
  );
543
550
  const mergeOptionPatch = useCallback(
@@ -593,16 +600,17 @@ export function PanelConfigSidebar({
593
600
  return (
594
601
  <ScopeConfigProvider
595
602
  scope={showScopePanel ? viewElementScope : undefined}
603
+ element={selectedElement}
596
604
  warnings={scopeWarnings}
597
605
  scrollContainerRef={sidebarScrollRef}
598
606
  >
599
607
  <aside
600
608
  ref={sidebarScrollRef}
601
- className={`scope-config-sidebar h-full overflow-auto border-l border-border bg-muted/[0.14] px-3 py-3 text-foreground [&_.scope-field--highlight]:rounded-md [&_.scope-field--highlight]:ring-2 [&_.scope-field--highlight]:ring-amber-400/80 [&_button[role=checkbox]]:border-2 [&_button[role=checkbox]]:border-foreground/80 [&_button[role=checkbox]]:bg-background [&_button[role=checkbox]]:ring-1 [&_button[role=checkbox]]:ring-foreground/40 [&_button[role=checkbox][data-state=checked]]:border-primary [&_button[role=checkbox][data-state=checked]]:ring-primary/40 ${themedScrollbarClass}`}
609
+ 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}`}
602
610
  >
603
611
  <div className="sticky top-0 z-20 mb-3 rounded-lg border border-border/70 bg-card/95 px-2.5 py-2 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-card/80">
604
612
  <div className="flex items-center justify-between gap-2">
605
- <div className="text-xs font-semibold tracking-wide">配置面板</div>
613
+ <div className="text-xs font-semibold tracking-wide">{t("panel.config.panelTitle")}</div>
606
614
  </div>
607
615
  {showScopePanel ? (
608
616
  <ViewElementScopePanel scope={viewElementScope} />
@@ -621,7 +629,7 @@ export function PanelConfigSidebar({
621
629
  className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
622
630
  onClick={() => setIsSearchCollapsed((prev) => !prev)}
623
631
  >
624
- {isSearchCollapsed ? "展开搜索" : "收起搜索"}
632
+ {isSearchCollapsed ? t("panel.config.expandSearch") : t("panel.config.collapseSearch")}
625
633
  </button>
626
634
  </div>
627
635
  {!isSearchCollapsed ? (
@@ -629,13 +637,13 @@ export function PanelConfigSidebar({
629
637
  <Input
630
638
  value={configSearch}
631
639
  onChange={(e) => setConfigSearch(e.target.value)}
632
- placeholder="搜索配置,如:边框、tooltip、音频、网格..."
640
+ placeholder={t("panel.config.searchPlaceholder")}
633
641
  className="h-7"
634
642
  data-scope-autocomplete="off"
635
643
  />
636
644
  {hasSearch ? (
637
645
  <div className="mt-1 text-[11px] text-muted-foreground">
638
- 搜索中:{configSearch}
646
+ {t("panel.config.searching", { query: configSearch })}
639
647
  </div>
640
648
  ) : null}
641
649
  </div>
@@ -646,7 +654,7 @@ export function PanelConfigSidebar({
646
654
  <div className="space-y-3">
647
655
  <Card>
648
656
  <CardHeader className="pb-2">
649
- <CardTitle className="text-xs">批量设置({effectiveSelectedElements.length} 个)</CardTitle>
657
+ <CardTitle className="text-xs">{t("panel.config.batchTitleWithCount", { count: effectiveSelectedElements.length })}</CardTitle>
650
658
  </CardHeader>
651
659
  <CardContent className="space-y-2 text-xs">
652
660
  <div className="grid grid-cols-2 gap-2">
@@ -655,14 +663,14 @@ export function PanelConfigSidebar({
655
663
  className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
656
664
  onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: true }))}
657
665
  >
658
- 全部锁定
666
+ {t("panel.config.lockAll")}
659
667
  </button>
660
668
  <button
661
669
  type="button"
662
670
  className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
663
671
  onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: false }))}
664
672
  >
665
- 全部解锁
673
+ {t("panel.config.unlockAll")}
666
674
  </button>
667
675
  <button
668
676
  type="button"
@@ -673,7 +681,7 @@ export function PanelConfigSidebar({
673
681
  )
674
682
  }
675
683
  >
676
- 全部上移一层
684
+ {t("panel.config.bringAllForward")}
677
685
  </button>
678
686
  <button
679
687
  type="button"
@@ -684,7 +692,7 @@ export function PanelConfigSidebar({
684
692
  )
685
693
  }
686
694
  >
687
- 全部下移一层
695
+ {t("panel.config.sendAllBackward")}
688
696
  </button>
689
697
  <button
690
698
  type="button"
@@ -695,7 +703,7 @@ export function PanelConfigSidebar({
695
703
  )
696
704
  }
697
705
  >
698
- 全部 zIndex 设为 1
706
+ {t("panel.config.setAllZIndex1")}
699
707
  </button>
700
708
  <button
701
709
  type="button"
@@ -711,7 +719,7 @@ export function PanelConfigSidebar({
711
719
  )
712
720
  }
713
721
  >
714
- 全部背景色设为蓝色
722
+ {t("panel.config.setAllBgBlue")}
715
723
  </button>
716
724
  </div>
717
725
  </CardContent>
@@ -734,20 +742,20 @@ export function PanelConfigSidebar({
734
742
  type="button"
735
743
  className="inline-flex h-6 w-6 items-center justify-center rounded border border-border text-[11px] hover:bg-accent"
736
744
  onClick={() => setNodeCardExpanded(el.id, !isNodeCardExpanded(el.id))}
737
- aria-label={isNodeCardExpanded(el.id) ? "收起节点配置" : "展开节点配置"}
745
+ aria-label={isNodeCardExpanded(el.id) ? t("panel.config.collapseNodeConfig") : t("panel.config.expandNodeConfig")}
738
746
  >
739
747
  {isNodeCardExpanded(el.id) ? "▾" : "▸"}
740
748
  </button>
741
749
  <CardTitle className="min-w-0 flex-1 text-xs truncate">
742
- {el.name?.trim() || el.materialType || "节点"} · {el.id}
750
+ {el.name?.trim() || el.materialType || t("common.node")} · {el.id}
743
751
  </CardTitle>
744
752
  <button
745
753
  type="button"
746
754
  className="inline-flex h-6 items-center justify-center rounded border border-border px-2 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
747
755
  onClick={() => onExcludeSelectedNode?.(el.id)}
748
- title="将该节点从当前多选中剔除"
756
+ title={t("panel.config.removeFromSelectionTitle")}
749
757
  >
750
- 剔除
758
+ {t("panel.config.removeFromSelection")}
751
759
  </button>
752
760
  </div>
753
761
  </CardHeader>
@@ -758,17 +766,17 @@ export function PanelConfigSidebar({
758
766
  checked={el.locked === true}
759
767
  onCheckedChange={(checked) => updateElement(el.id, { locked: checked === true })}
760
768
  />
761
- <span>锁定节点</span>
769
+ <span>{t("panel.config.lockedNode")}</span>
762
770
  </label>
763
771
  {el.locked ? (
764
772
  <div className="rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] text-amber-700 dark:text-amber-300">
765
- 当前节点已锁定,仅可操作锁定开关。
773
+ {t("panel.config.lockedNodeHint")}
766
774
  </div>
767
775
  ) : null}
768
776
  <fieldset disabled={el.locked} className={el.locked ? "opacity-60" : ""}>
769
777
  <div className="space-y-2">
770
778
  <label className="block space-y-1">
771
- <div>名称</div>
779
+ <div>{t("panel.config.name")}</div>
772
780
  <Input
773
781
  className="h-7"
774
782
  value={el.name ?? ""}
@@ -788,13 +796,13 @@ export function PanelConfigSidebar({
788
796
  />
789
797
  </label>
790
798
  <label className="block space-y-1">
791
- <div>图层</div>
799
+ <div>{t("panel.config.layer")}</div>
792
800
  <Select
793
801
  value={el.layerId}
794
802
  onValueChange={(value) => updateElement(el.id, { layerId: value })}
795
803
  >
796
804
  <SelectTrigger className="h-7">
797
- <SelectValue placeholder="选择图层" />
805
+ <SelectValue placeholder={t("panel.config.selectLayer")} />
798
806
  </SelectTrigger>
799
807
  <SelectContent>
800
808
  {layers.map((layer) => (
@@ -826,7 +834,7 @@ export function PanelConfigSidebar({
826
834
  />
827
835
  </label>
828
836
  <label className="block space-y-1">
829
- <div>旋转角度</div>
837
+ <div>{t("panel.config.rotate")}</div>
830
838
  <Input
831
839
  className="h-7"
832
840
  type="number"
@@ -837,7 +845,7 @@ export function PanelConfigSidebar({
837
845
  />
838
846
  </label>
839
847
  <label className="block space-y-1">
840
- <div>宽</div>
848
+ <div>{t("panel.config.width")}</div>
841
849
  <Input
842
850
  className="h-7"
843
851
  type="number"
@@ -849,7 +857,7 @@ export function PanelConfigSidebar({
849
857
  />
850
858
  </label>
851
859
  <label className="block space-y-1">
852
- <div>高</div>
860
+ <div>{t("panel.config.height")}</div>
853
861
  <Input
854
862
  className="h-7"
855
863
  type="number"
@@ -863,7 +871,7 @@ export function PanelConfigSidebar({
863
871
  </div>
864
872
  <div className="grid grid-cols-2 gap-2">
865
873
  <label className="block space-y-1">
866
- <div>背景色</div>
874
+ <div>{t("panel.config.backgroundColor")}</div>
867
875
  <Input
868
876
  className="h-7"
869
877
  value={el.style?.backgroundColor ?? ""}
@@ -879,7 +887,7 @@ export function PanelConfigSidebar({
879
887
  />
880
888
  </label>
881
889
  <label className="block space-y-1">
882
- <div>边框色</div>
890
+ <div>{t("panel.config.borderColorShort")}</div>
883
891
  <Input
884
892
  className="h-7"
885
893
  value={el.style?.borderColor ?? ""}
@@ -898,7 +906,7 @@ export function PanelConfigSidebar({
898
906
  {CHART_TYPES.has(el.materialType ?? "") ? (
899
907
  <div className="grid grid-cols-2 gap-2">
900
908
  <label className="block space-y-1 col-span-2">
901
- <div>图表标题</div>
909
+ <div>{t("panel.config.chartTitle")}</div>
902
910
  <Input
903
911
  className="h-7"
904
912
  value={el.chart?.title ?? ""}
@@ -910,7 +918,7 @@ export function PanelConfigSidebar({
910
918
  />
911
919
  </label>
912
920
  <label className="block space-y-1">
913
- <div>主色</div>
921
+ <div>{t("panel.config.primaryColor")}</div>
914
922
  <Input
915
923
  className="h-7"
916
924
  value={el.chart?.color ?? ""}
@@ -930,12 +938,12 @@ export function PanelConfigSidebar({
930
938
  })
931
939
  }
932
940
  />
933
- <span>主色渐变</span>
941
+ <span>{t("panel.config.primaryGradient")}</span>
934
942
  </label>
935
943
  {el.chart?.colorMode === "gradient" ? (
936
944
  <>
937
945
  <label className="block space-y-1">
938
- <div>渐变起始色</div>
946
+ <div>{t("panel.config.gradientFrom")}</div>
939
947
  <Input
940
948
  className="h-7"
941
949
  value={el.chart?.gradientFrom ?? el.chart?.color ?? "#3b82f6"}
@@ -947,7 +955,7 @@ export function PanelConfigSidebar({
947
955
  />
948
956
  </label>
949
957
  <label className="block space-y-1">
950
- <div>渐变结束色</div>
958
+ <div>{t("panel.config.gradientTo")}</div>
951
959
  <Input
952
960
  className="h-7"
953
961
  value={el.chart?.gradientTo ?? "#22d3ee"}
@@ -959,7 +967,7 @@ export function PanelConfigSidebar({
959
967
  />
960
968
  </label>
961
969
  <label className="block space-y-1 col-span-2">
962
- <div>渐变方向</div>
970
+ <div>{t("panel.config.gradientDirection")}</div>
963
971
  <Select
964
972
  value={el.chart?.gradientDirection ?? "to-right"}
965
973
  onValueChange={(value) =>
@@ -977,15 +985,15 @@ export function PanelConfigSidebar({
977
985
  >
978
986
  <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
979
987
  <SelectContent>
980
- <SelectItem value="to-right">左 → 右</SelectItem>
981
- <SelectItem value="to-bottom">上 → 下</SelectItem>
982
- <SelectItem value="to-bottom-right">左上 → 右下</SelectItem>
983
- <SelectItem value="to-top-right">左下 → 右上</SelectItem>
988
+ <SelectItem value="to-right">{t("panel.config.dirToRight")}</SelectItem>
989
+ <SelectItem value="to-bottom">{t("panel.config.dirToBottom")}</SelectItem>
990
+ <SelectItem value="to-bottom-right">{t("panel.config.dirToBottomRight")}</SelectItem>
991
+ <SelectItem value="to-top-right">{t("panel.config.dirToTopRight")}</SelectItem>
984
992
  </SelectContent>
985
993
  </Select>
986
994
  </label>
987
995
  <div className="col-span-2 space-y-1">
988
- <div className="text-[11px] text-muted-foreground">渐变预览</div>
996
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.gradientPreview")}</div>
989
997
  <div
990
998
  className="h-6 rounded border border-border/60"
991
999
  style={{
@@ -1004,7 +1012,7 @@ export function PanelConfigSidebar({
1004
1012
  </>
1005
1013
  ) : null}
1006
1014
  <label className="block space-y-1">
1007
- <div>渲染</div>
1015
+ <div>{t("panel.config.render")}</div>
1008
1016
  <Select
1009
1017
  value={el.chart?.renderMode ?? "canvas"}
1010
1018
  onValueChange={(value) =>
@@ -1023,7 +1031,7 @@ export function PanelConfigSidebar({
1023
1031
  </Select>
1024
1032
  </label>
1025
1033
  <label className="block space-y-1">
1026
- <div>Tooltip 背景</div>
1034
+ <div>{t("panel.config.tooltipBg")}</div>
1027
1035
  <Input
1028
1036
  className="h-7"
1029
1037
  value={el.chart?.tooltipBackgroundColor ?? ""}
@@ -1047,10 +1055,10 @@ export function PanelConfigSidebar({
1047
1055
  })
1048
1056
  }
1049
1057
  />
1050
- <span>显示 Tooltip</span>
1058
+ <span>{t("panel.config.showTooltip")}</span>
1051
1059
  </label>
1052
1060
  <label className="block space-y-1">
1053
- <div>Tooltip 触发方式</div>
1061
+ <div>{t("panel.config.tooltipTrigger")}</div>
1054
1062
  <Select
1055
1063
  value={el.chart?.tooltipTrigger ?? "axis"}
1056
1064
  onValueChange={(value) =>
@@ -1067,7 +1075,7 @@ export function PanelConfigSidebar({
1067
1075
  </Select>
1068
1076
  </label>
1069
1077
  <label className="block space-y-1">
1070
- <div>Tooltip 文字色</div>
1078
+ <div>{t("panel.config.tooltipTextColor")}</div>
1071
1079
  <Input
1072
1080
  className="h-7"
1073
1081
  value={el.chart?.tooltipTextColor ?? ""}
@@ -1099,25 +1107,22 @@ export function PanelConfigSidebar({
1099
1107
  />
1100
1108
  </label>
1101
1109
  <label className="block space-y-1 col-span-2">
1102
- <div>类目(逗号分隔)</div>
1110
+ <div>{t("panel.config.labelsCsv")}</div>
1103
1111
  <Input
1104
1112
  className="h-7"
1105
- value={(el.chart?.labels ?? []).join(",")}
1113
+ value={getChartLabelsDisplayText(el.chart)}
1106
1114
  onChange={(e) =>
1107
1115
  updateElement(el.id, {
1108
1116
  chart: {
1109
1117
  ...(el.chart ?? {}),
1110
- labels: e.target.value
1111
- .split(",")
1112
- .map((s) => s.trim())
1113
- .filter(Boolean),
1118
+ labelsText: e.target.value,
1114
1119
  },
1115
1120
  })
1116
1121
  }
1117
1122
  />
1118
1123
  </label>
1119
1124
  <label className="block space-y-1 col-span-2">
1120
- <div>数值(逗号分隔)</div>
1125
+ <div>{t("panel.config.valuesCsv")}</div>
1121
1126
  <Input
1122
1127
  className="h-7"
1123
1128
  value={getChartValuesDisplayText(el.chart)}
@@ -1134,7 +1139,7 @@ export function PanelConfigSidebar({
1134
1139
  {["bar", "line", "area", "scatter"].includes(el.materialType ?? "") ? (
1135
1140
  <>
1136
1141
  <label className="block space-y-1">
1137
- <div>X 轴名称</div>
1142
+ <div>{t("panel.config.xAxisName")}</div>
1138
1143
  <Input
1139
1144
  className="h-7"
1140
1145
  value={el.chart?.xAxisName ?? ""}
@@ -1146,7 +1151,7 @@ export function PanelConfigSidebar({
1146
1151
  />
1147
1152
  </label>
1148
1153
  <label className="block space-y-1">
1149
- <div>Y 轴名称</div>
1154
+ <div>{t("panel.config.yAxisName")}</div>
1150
1155
  <Input
1151
1156
  className="h-7"
1152
1157
  value={el.chart?.yAxisName ?? ""}
@@ -1158,7 +1163,7 @@ export function PanelConfigSidebar({
1158
1163
  />
1159
1164
  </label>
1160
1165
  <label className="block space-y-1">
1161
- <div>X 轴标签颜色</div>
1166
+ <div>{t("panel.config.xAxisLabelColor")}</div>
1162
1167
  <Input
1163
1168
  className="h-7"
1164
1169
  value={el.chart?.xAxisLabelColor ?? ""}
@@ -1171,7 +1176,7 @@ export function PanelConfigSidebar({
1171
1176
  />
1172
1177
  </label>
1173
1178
  <label className="block space-y-1">
1174
- <div>Y 轴标签颜色</div>
1179
+ <div>{t("panel.config.yAxisLabelColor")}</div>
1175
1180
  <Input
1176
1181
  className="h-7"
1177
1182
  value={el.chart?.yAxisLabelColor ?? ""}
@@ -1184,7 +1189,7 @@ export function PanelConfigSidebar({
1184
1189
  />
1185
1190
  </label>
1186
1191
  <label className="block space-y-1">
1187
- <div>X 轴标签字号</div>
1192
+ <div>{t("panel.config.xAxisLabelFontSize")}</div>
1188
1193
  <Input
1189
1194
  className="h-7"
1190
1195
  type="number"
@@ -1202,7 +1207,7 @@ export function PanelConfigSidebar({
1202
1207
  />
1203
1208
  </label>
1204
1209
  <label className="block space-y-1">
1205
- <div>Y 轴标签字号</div>
1210
+ <div>{t("panel.config.yAxisLabelFontSize")}</div>
1206
1211
  <Input
1207
1212
  className="h-7"
1208
1213
  type="number"
@@ -1228,7 +1233,7 @@ export function PanelConfigSidebar({
1228
1233
  })
1229
1234
  }
1230
1235
  />
1231
- <span>X 轴标签自动缩略</span>
1236
+ <span>{t("panel.config.xAxisLabelAutoEllipsis")}</span>
1232
1237
  </label>
1233
1238
  <label className="flex items-center gap-2">
1234
1239
  <Checkbox
@@ -1239,7 +1244,7 @@ export function PanelConfigSidebar({
1239
1244
  })
1240
1245
  }
1241
1246
  />
1242
- <span>Y 轴标签自动缩略</span>
1247
+ <span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
1243
1248
  </label>
1244
1249
  <label className="flex items-center gap-2">
1245
1250
  <Checkbox
@@ -1250,7 +1255,7 @@ export function PanelConfigSidebar({
1250
1255
  })
1251
1256
  }
1252
1257
  />
1253
- <span>X 轴刻度线</span>
1258
+ <span>{t("panel.config.xAxisTick")}</span>
1254
1259
  </label>
1255
1260
  <label className="flex items-center gap-2">
1256
1261
  <Checkbox
@@ -1261,10 +1266,10 @@ export function PanelConfigSidebar({
1261
1266
  })
1262
1267
  }
1263
1268
  />
1264
- <span>Y 轴刻度线</span>
1269
+ <span>{t("panel.config.yAxisTick")}</span>
1265
1270
  </label>
1266
1271
  <label className="block space-y-1">
1267
- <div>X 轴刻度线颜色</div>
1272
+ <div>{t("panel.config.xAxisTickColor")}</div>
1268
1273
  <Input
1269
1274
  className="h-7"
1270
1275
  value={el.chart?.xAxisTickColor ?? ""}
@@ -1277,7 +1282,7 @@ export function PanelConfigSidebar({
1277
1282
  />
1278
1283
  </label>
1279
1284
  <label className="block space-y-1">
1280
- <div>Y 轴刻度线颜色</div>
1285
+ <div>{t("panel.config.yAxisTickColor")}</div>
1281
1286
  <Input
1282
1287
  className="h-7"
1283
1288
  value={el.chart?.yAxisTickColor ?? ""}
@@ -1298,7 +1303,7 @@ export function PanelConfigSidebar({
1298
1303
  })
1299
1304
  }
1300
1305
  />
1301
- <span>X 轴分割线</span>
1306
+ <span>{t("panel.config.xAxisSplitLine")}</span>
1302
1307
  </label>
1303
1308
  <label className="flex items-center gap-2">
1304
1309
  <Checkbox
@@ -1309,10 +1314,10 @@ export function PanelConfigSidebar({
1309
1314
  })
1310
1315
  }
1311
1316
  />
1312
- <span>Y 轴分割线</span>
1317
+ <span>{t("panel.config.yAxisSplitLine")}</span>
1313
1318
  </label>
1314
1319
  <label className="block space-y-1">
1315
- <div>X 轴分割线颜色</div>
1320
+ <div>{t("panel.config.xAxisSplitLineColor")}</div>
1316
1321
  <Input
1317
1322
  className="h-7"
1318
1323
  value={el.chart?.xAxisSplitLineColor ?? ""}
@@ -1325,7 +1330,7 @@ export function PanelConfigSidebar({
1325
1330
  />
1326
1331
  </label>
1327
1332
  <label className="block space-y-1">
1328
- <div>Y 轴分割线颜色</div>
1333
+ <div>{t("panel.config.yAxisSplitLineColor")}</div>
1329
1334
  <Input
1330
1335
  className="h-7"
1331
1336
  value={el.chart?.yAxisSplitLineColor ?? ""}
@@ -1341,7 +1346,7 @@ export function PanelConfigSidebar({
1341
1346
  ) : null}
1342
1347
  {el.materialType === "gauge" ? (
1343
1348
  <label className="block space-y-1">
1344
- <div>仪表盘值</div>
1349
+ <div>{t("panel.config.gaugeValue")}</div>
1345
1350
  <Input
1346
1351
  className="h-7"
1347
1352
  type="number"
@@ -1359,7 +1364,7 @@ export function PanelConfigSidebar({
1359
1364
  ) : null}
1360
1365
  {el.materialType === "bar" ? (
1361
1366
  <label className="block space-y-1">
1362
- <div>柱宽</div>
1367
+ <div>{t("panel.config.barWidth")}</div>
1363
1368
  <Input
1364
1369
  className="h-7"
1365
1370
  type="number"
@@ -1386,13 +1391,13 @@ export function PanelConfigSidebar({
1386
1391
  })
1387
1392
  }
1388
1393
  />
1389
- <span>平滑曲线</span>
1394
+ <span>{t("panel.config.smooth")}</span>
1390
1395
  </label>
1391
1396
  ) : null}
1392
1397
  {el.materialType === "pie" ? (
1393
1398
  <>
1394
1399
  <label className="block space-y-1">
1395
- <div>内半径</div>
1400
+ <div>{t("panel.config.pieInnerRadius")}</div>
1396
1401
  <Input
1397
1402
  className="h-7"
1398
1403
  type="number"
@@ -1410,7 +1415,7 @@ export function PanelConfigSidebar({
1410
1415
  />
1411
1416
  </label>
1412
1417
  <label className="block space-y-1">
1413
- <div>外半径</div>
1418
+ <div>{t("panel.config.pieOuterRadius")}</div>
1414
1419
  <Input
1415
1420
  className="h-7"
1416
1421
  type="number"
@@ -1430,7 +1435,7 @@ export function PanelConfigSidebar({
1430
1435
  </>
1431
1436
  ) : null}
1432
1437
  <label className="block space-y-1">
1433
- {renderOptionLabel("网格左边距", "grid.left", "控制绘图区左侧留白。")}
1438
+ {renderOptionLabel(t("panel.config.gridLeft"), "grid.left", t("panel.config.gridLeftHint"))}
1434
1439
  <Input
1435
1440
  className="h-7"
1436
1441
  type="number"
@@ -1448,7 +1453,7 @@ export function PanelConfigSidebar({
1448
1453
  />
1449
1454
  </label>
1450
1455
  <label className="block space-y-1">
1451
- {renderOptionLabel("网格右边距", "grid.right", "控制绘图区右侧留白。")}
1456
+ {renderOptionLabel(t("panel.config.gridRight"), "grid.right", t("panel.config.gridRightHint"))}
1452
1457
  <Input
1453
1458
  className="h-7"
1454
1459
  type="number"
@@ -1466,7 +1471,7 @@ export function PanelConfigSidebar({
1466
1471
  />
1467
1472
  </label>
1468
1473
  <label className="block space-y-1">
1469
- {renderOptionLabel("网格上边距", "grid.top", "控制绘图区顶部留白。")}
1474
+ {renderOptionLabel(t("panel.config.gridTop"), "grid.top", t("panel.config.gridTopHint"))}
1470
1475
  <Input
1471
1476
  className="h-7"
1472
1477
  type="number"
@@ -1484,7 +1489,7 @@ export function PanelConfigSidebar({
1484
1489
  />
1485
1490
  </label>
1486
1491
  <label className="block space-y-1">
1487
- {renderOptionLabel("网格下边距", "grid.bottom", "控制绘图区底部留白。")}
1492
+ {renderOptionLabel(t("panel.config.gridBottom"), "grid.bottom", t("panel.config.gridBottomHint"))}
1488
1493
  <Input
1489
1494
  className="h-7"
1490
1495
  type="number"
@@ -1502,7 +1507,7 @@ export function PanelConfigSidebar({
1502
1507
  />
1503
1508
  </label>
1504
1509
  <label className="block space-y-1">
1505
- {renderOptionLabel("图例位置", "legend.top", "设置图例在容器中的停靠位置。")}
1510
+ {renderOptionLabel(t("panel.config.legendPosition"), "legend.top", t("panel.config.legendPositionHint"))}
1506
1511
  <Select
1507
1512
  value={String((el.chart?.option as any)?.legend?.top ?? "top")}
1508
1513
  onValueChange={(value) =>
@@ -1526,7 +1531,7 @@ export function PanelConfigSidebar({
1526
1531
  </Select>
1527
1532
  </label>
1528
1533
  <label className="block space-y-1">
1529
- {renderOptionLabel("图例排列", "legend.orient", "图例横向或纵向排列。")}
1534
+ {renderOptionLabel(t("panel.config.legendOrient"), "legend.orient", t("panel.config.legendOrientHint"))}
1530
1535
  <Select
1531
1536
  value={String((el.chart?.option as any)?.legend?.orient ?? "horizontal")}
1532
1537
  onValueChange={(value) =>
@@ -1561,7 +1566,7 @@ export function PanelConfigSidebar({
1561
1566
  })
1562
1567
  }
1563
1568
  />
1564
- <span>显示图例(legend.show)</span>
1569
+ <span>{t("panel.config.showLegendWithKey")}</span>
1565
1570
  </label>
1566
1571
  <label className="flex items-center gap-2">
1567
1572
  <Checkbox
@@ -1584,7 +1589,7 @@ export function PanelConfigSidebar({
1584
1589
  });
1585
1590
  }}
1586
1591
  />
1587
- <span>内置缩放(dataZoom[type=inside])</span>
1592
+ <span>{t("panel.config.zoomInsideWithKey")}</span>
1588
1593
  </label>
1589
1594
  <label className="flex items-center gap-2">
1590
1595
  <Checkbox
@@ -1607,7 +1612,7 @@ export function PanelConfigSidebar({
1607
1612
  });
1608
1613
  }}
1609
1614
  />
1610
- <span>滑块缩放(dataZoom[type=slider])</span>
1615
+ <span>{t("panel.config.zoomSliderWithKey")}</span>
1611
1616
  </label>
1612
1617
  <label className="flex items-center gap-2">
1613
1618
  <Checkbox
@@ -1623,10 +1628,10 @@ export function PanelConfigSidebar({
1623
1628
  })
1624
1629
  }
1625
1630
  />
1626
- <span>显示轴指示器(axisPointer.show)</span>
1631
+ <span>{t("panel.config.showAxisPointerWithKey")}</span>
1627
1632
  </label>
1628
1633
  <label className="block space-y-1">
1629
- <div>轴指示器类型(axisPointer.type)</div>
1634
+ <div>{t("panel.config.axisPointerTypeWithKey")}</div>
1630
1635
  <Select
1631
1636
  value={String((el.chart?.option as any)?.axisPointer?.type ?? "line")}
1632
1637
  onValueChange={(value) =>
@@ -1649,11 +1654,11 @@ export function PanelConfigSidebar({
1649
1654
  </Select>
1650
1655
  </label>
1651
1656
  <label className="block space-y-1 col-span-2">
1652
- <div>高级 option JSON(覆盖基础配置)</div>
1657
+ <div>{t("panel.config.advancedOptionJson")}</div>
1653
1658
  <Textarea
1654
1659
  className="h-28 font-mono text-[11px]"
1655
1660
  defaultValue={JSON.stringify(el.chart?.option ?? {}, null, 2)}
1656
- placeholder='例如:{"grid":{"left":24}}'
1661
+ placeholder={t("panel.config.advancedJsonPlaceholder")}
1657
1662
  onBlur={(e) => {
1658
1663
  const nextText = e.target.value.trim();
1659
1664
  if (!nextText) {
@@ -1674,7 +1679,7 @@ export function PanelConfigSidebar({
1674
1679
  {el.materialType === "text" ? (
1675
1680
  <div className="grid grid-cols-2 gap-2">
1676
1681
  <label className="block space-y-1 col-span-2">
1677
- <div>文本内容(HTML)</div>
1682
+ <div>{t("panel.config.textHtml")}</div>
1678
1683
  <Textarea
1679
1684
  className="h-24"
1680
1685
  value={el.textHtml ?? ""}
@@ -1682,7 +1687,7 @@ export function PanelConfigSidebar({
1682
1687
  />
1683
1688
  </label>
1684
1689
  <label className="block space-y-1">
1685
- <div>字体大小</div>
1690
+ <div>{t("panel.config.fontSize")}</div>
1686
1691
  <Input
1687
1692
  className="h-7"
1688
1693
  type="number"
@@ -1692,7 +1697,7 @@ export function PanelConfigSidebar({
1692
1697
  />
1693
1698
  </label>
1694
1699
  <label className="block space-y-1">
1695
- <div>文字颜色</div>
1700
+ <div>{t("panel.config.textColor")}</div>
1696
1701
  <Input
1697
1702
  className="h-7"
1698
1703
  value={el.textColor ?? ""}
@@ -1704,7 +1709,7 @@ export function PanelConfigSidebar({
1704
1709
  {el.materialType === "audio" ? (
1705
1710
  <div className="grid grid-cols-2 gap-2">
1706
1711
  <label className="block space-y-1 col-span-2">
1707
- <div>音频 URL</div>
1712
+ <div>{t("panel.config.audioUrl")}</div>
1708
1713
  <Input
1709
1714
  className="h-7"
1710
1715
  value={el.audioRemoteUrl ?? ""}
@@ -1712,7 +1717,7 @@ export function PanelConfigSidebar({
1712
1717
  />
1713
1718
  </label>
1714
1719
  <label className="block space-y-1">
1715
- <div>动效</div>
1720
+ <div>{t("panel.config.effect")}</div>
1716
1721
  <Select
1717
1722
  value={el.audioVisualEffect ?? "pulse"}
1718
1723
  onValueChange={(value) =>
@@ -1728,7 +1733,7 @@ export function PanelConfigSidebar({
1728
1733
  </Select>
1729
1734
  </label>
1730
1735
  <label className="block space-y-1">
1731
- <div>速度</div>
1736
+ <div>{t("panel.config.speed")}</div>
1732
1737
  <Select
1733
1738
  value={el.audioVisualSpeed ?? "normal"}
1734
1739
  onValueChange={(value) =>
@@ -1747,7 +1752,7 @@ export function PanelConfigSidebar({
1747
1752
  ) : null}
1748
1753
  {el.materialType === "video" ? (
1749
1754
  <label className="block space-y-1">
1750
- <div>视频 URL</div>
1755
+ <div>{t("panel.config.videoUrl")}</div>
1751
1756
  <Input
1752
1757
  className="h-7"
1753
1758
  value={el.videoRemoteUrl ?? ""}
@@ -1758,11 +1763,11 @@ export function PanelConfigSidebar({
1758
1763
  {el.materialType === "grid" ? (
1759
1764
  <div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
1760
1765
  <label className="block space-y-1.5">
1761
- <div>行</div>
1766
+ <div>{t("panel.config.rows")}</div>
1762
1767
  <Input className="h-7" type="number" min={1} value={el.gridRows ?? 2} onChange={(e) => updateElement(el.id, { gridRows: Math.max(1, Number(e.target.value) || 2) })} />
1763
1768
  </label>
1764
1769
  <label className="block space-y-1.5">
1765
- <div>列</div>
1770
+ <div>{t("panel.config.cols")}</div>
1766
1771
  <Input className="h-7" type="number" min={1} value={el.gridCols ?? 3} onChange={(e) => updateElement(el.id, { gridCols: Math.max(1, Number(e.target.value) || 3) })} />
1767
1772
  </label>
1768
1773
  </div>
@@ -1770,7 +1775,7 @@ export function PanelConfigSidebar({
1770
1775
  {el.materialType === "geometry" ? (
1771
1776
  <div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
1772
1777
  <label className="block space-y-1.5">
1773
- <div>形状</div>
1778
+ <div>{t("panel.config.shape")}</div>
1774
1779
  <Select
1775
1780
  value={el.geometryShape ?? "rect"}
1776
1781
  onValueChange={(value) =>
@@ -1779,18 +1784,18 @@ export function PanelConfigSidebar({
1779
1784
  >
1780
1785
  <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1781
1786
  <SelectContent>
1782
- <SelectItem value="rect">矩形</SelectItem>
1783
- <SelectItem value="circle">圆形</SelectItem>
1784
- <SelectItem value="triangle">三角形</SelectItem>
1785
- <SelectItem value="diamond">菱形</SelectItem>
1786
- <SelectItem value="hexagon">六边形</SelectItem>
1787
- <SelectItem value="star">星形</SelectItem>
1788
- <SelectItem value="heart">爱心</SelectItem>
1787
+ <SelectItem value="rect">{t("panel.config.shapeRect")}</SelectItem>
1788
+ <SelectItem value="circle">{t("panel.config.shapeCircle")}</SelectItem>
1789
+ <SelectItem value="triangle">{t("panel.config.shapeTriangle")}</SelectItem>
1790
+ <SelectItem value="diamond">{t("panel.config.shapeDiamond")}</SelectItem>
1791
+ <SelectItem value="hexagon">{t("panel.config.shapeHexagon")}</SelectItem>
1792
+ <SelectItem value="star">{t("panel.config.shapeStar")}</SelectItem>
1793
+ <SelectItem value="heart">{t("panel.config.shapeHeart")}</SelectItem>
1789
1794
  </SelectContent>
1790
1795
  </Select>
1791
1796
  </label>
1792
1797
  <label className="block space-y-1.5">
1793
- <div>颜色</div>
1798
+ <div>{t("panel.config.color")}</div>
1794
1799
  <Input
1795
1800
  className="h-7"
1796
1801
  value={el.geometryColor ?? "#3b82f6"}
@@ -1802,14 +1807,14 @@ export function PanelConfigSidebar({
1802
1807
  {el.materialType === "reference" ? (
1803
1808
  <div className="grid grid-cols-2 gap-2">
1804
1809
  <label className="block space-y-1.5">
1805
- <div>引用图层</div>
1810
+ <div>{t("panel.config.refLayer")}</div>
1806
1811
  <Select
1807
1812
  value={el.refLayerId ?? "__none__"}
1808
1813
  onValueChange={(value) => updateElement(el.id, { refLayerId: value === "__none__" ? undefined : value })}
1809
1814
  >
1810
1815
  <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1811
1816
  <SelectContent>
1812
- <SelectItem value="__none__">无</SelectItem>
1817
+ <SelectItem value="__none__">{t("common.none")}</SelectItem>
1813
1818
  {layers.filter((l) => l.id !== el.layerId).map((l) => (
1814
1819
  <SelectItem key={l.id} value={l.id}>{l.name}</SelectItem>
1815
1820
  ))}
@@ -1817,7 +1822,7 @@ export function PanelConfigSidebar({
1817
1822
  </Select>
1818
1823
  </label>
1819
1824
  <label className="block space-y-1.5">
1820
- <div>拷贝</div>
1825
+ <div>{t("panel.config.copy")}</div>
1821
1826
  <Select
1822
1827
  value={el.refCopyMode ?? "shallow"}
1823
1828
  onValueChange={(value) => setReferenceCopyMode?.(el.id, value as ReferenceCopyMode)}
@@ -1844,7 +1849,7 @@ export function PanelConfigSidebar({
1844
1849
  return !text.includes(normalizedSearch);
1845
1850
  }) ? (
1846
1851
  <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
1847
- 未匹配到可编辑节点,请更换关键词。
1852
+ {t("panel.config.noEditableMatch")}
1848
1853
  </div>
1849
1854
  ) : null}
1850
1855
  </div>
@@ -1864,8 +1869,8 @@ export function PanelConfigSidebar({
1864
1869
  <path d="M8 9h8M8 12h8M8 15h5" />
1865
1870
  </svg>
1866
1871
  </EmptyIcon>
1867
- <EmptyTitle>暂无可配置节点</EmptyTitle>
1868
- <EmptyDescription>请先在画布中选中一个节点,再到这里进行配置。</EmptyDescription>
1872
+ <EmptyTitle>{t("panel.config.emptyNoNodeTitle")}</EmptyTitle>
1873
+ <EmptyDescription>{t("panel.config.emptyNoNodeDesc")}</EmptyDescription>
1869
1874
  </Empty>
1870
1875
  ) : !isMultiSelectMode && selectedElement ? (
1871
1876
  <div className="space-y-3">
@@ -1886,17 +1891,17 @@ export function PanelConfigSidebar({
1886
1891
  })
1887
1892
  }
1888
1893
  />
1889
- <span>锁定节点(禁止层级/位置/大小/旋转)</span>
1894
+ <span>{t("panel.config.lockNode")}</span>
1890
1895
  </label>
1891
1896
  </div>
1892
1897
  <fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
1893
1898
  <div className="space-y-3.5 text-xs">
1894
1899
  {renderSection(
1895
1900
  "nodeInfo",
1896
- "节点信息",
1901
+ t("panel.config.sectionNodeInfo"),
1897
1902
  <>
1898
- <label className="block space-y-1">
1899
- <div>节点名称</div>
1903
+ <label className="block space-y-1" data-config-field="name">
1904
+ <div>{t("panel.config.nodeName")}</div>
1900
1905
  <Input
1901
1906
  value={selectedElement.name ?? ""}
1902
1907
  onChange={(e) =>
@@ -1904,12 +1909,12 @@ export function PanelConfigSidebar({
1904
1909
  name: e.target.value || undefined,
1905
1910
  })
1906
1911
  }
1907
- placeholder="自定义节点名称(显示在节点树)"
1912
+ placeholder={t("panel.config.nodeNamePlaceholder")}
1908
1913
  className="h-7"
1909
1914
  />
1910
1915
  </label>
1911
1916
  <div className="grid grid-cols-3 gap-2">
1912
- <label className="block space-y-1">
1917
+ <label className="block space-y-1" data-config-field="x">
1913
1918
  <div>X</div>
1914
1919
  <Input
1915
1920
  type="number"
@@ -1922,7 +1927,7 @@ export function PanelConfigSidebar({
1922
1927
  className="h-7"
1923
1928
  />
1924
1929
  </label>
1925
- <label className="block space-y-1">
1930
+ <label className="block space-y-1" data-config-field="y">
1926
1931
  <div>Y</div>
1927
1932
  <Input
1928
1933
  type="number"
@@ -1937,8 +1942,8 @@ export function PanelConfigSidebar({
1937
1942
  </label>
1938
1943
  </div>
1939
1944
  <div className="grid grid-cols-3 gap-2">
1940
- <label className="block space-y-1">
1941
- <div>旋转角度</div>
1945
+ <label className="block space-y-1" data-config-field="rotate">
1946
+ <div>{t("panel.config.rotate")}</div>
1942
1947
  <Input
1943
1948
  type="number"
1944
1949
  value={selectedElement.rotate ?? 0}
@@ -1950,8 +1955,8 @@ export function PanelConfigSidebar({
1950
1955
  className="h-7"
1951
1956
  />
1952
1957
  </label>
1953
- <label className="block space-y-1">
1954
- <div>宽</div>
1958
+ <label className="block space-y-1" data-config-field="width">
1959
+ <div>{t("panel.config.width")}</div>
1955
1960
  <Input
1956
1961
  type="number"
1957
1962
  min={1}
@@ -1965,8 +1970,8 @@ export function PanelConfigSidebar({
1965
1970
  className="h-7"
1966
1971
  />
1967
1972
  </label>
1968
- <label className="block space-y-1">
1969
- <div>高</div>
1973
+ <label className="block space-y-1" data-config-field="height">
1974
+ <div>{t("panel.config.height")}</div>
1970
1975
  <Input
1971
1976
  type="number"
1972
1977
  min={1}
@@ -1981,9 +1986,9 @@ export function PanelConfigSidebar({
1981
1986
  </label>
1982
1987
  </div>
1983
1988
  <div className="space-y-1.5">
1984
- <div className="text-[11px] text-muted-foreground">节点层级</div>
1989
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.nodeZOrder")}</div>
1985
1990
  <div className="text-[11px] text-muted-foreground/90">
1986
- 当前 zIndex:{nodeZOrderLabel ?? "-"}
1991
+ {t("panel.config.currentZIndex", { value: nodeZOrderLabel ?? "-" })}
1987
1992
  </div>
1988
1993
  <div className="grid grid-cols-2 gap-2">
1989
1994
  <button
@@ -1991,52 +1996,52 @@ export function PanelConfigSidebar({
1991
1996
  className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
1992
1997
  onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "bringForward")}
1993
1998
  >
1994
- 上移一层
1999
+ {t("panel.menubar.bringForward")}
1995
2000
  </button>
1996
2001
  <button
1997
2002
  type="button"
1998
2003
  className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
1999
2004
  onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "sendBackward")}
2000
2005
  >
2001
- 下移一层
2006
+ {t("panel.menubar.sendBackward")}
2002
2007
  </button>
2003
2008
  <button
2004
2009
  type="button"
2005
2010
  className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
2006
2011
  onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "bringToFront")}
2007
2012
  >
2008
- 置顶
2013
+ {t("panel.menubar.bringToFront")}
2009
2014
  </button>
2010
2015
  <button
2011
2016
  type="button"
2012
2017
  className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
2013
2018
  onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "sendToBack")}
2014
2019
  >
2015
- 置底
2020
+ {t("panel.menubar.sendToBack")}
2016
2021
  </button>
2017
2022
  </div>
2018
2023
  </div>
2019
2024
  <div className="truncate text-muted-foreground">ID: {selectedElement.id}</div>
2020
- <div className="text-muted-foreground">类型: {selectedElement.materialType ?? selectedElement.id}</div>
2025
+ <div className="text-muted-foreground">{t("panel.config.type")}: {selectedElement.materialType ?? selectedElement.id}</div>
2021
2026
  </>,
2022
2027
  true,
2023
- ["名称", "id", "类型", "锁定", "locked", "name"]
2028
+ [t("panel.config.name"), "id", t("panel.config.type"), t("panel.layers.lockShort"), "locked", "name"]
2024
2029
  )}
2025
2030
 
2026
2031
  {renderSection(
2027
2032
  "styleBackground",
2028
- "通用样式 / 背景",
2033
+ t("panel.config.sectionStyleBackground"),
2029
2034
  <>
2030
2035
  {renderFieldGroup(
2031
- "背景填充",
2036
+ t("panel.config.groupBgFill"),
2032
2037
  <>
2033
2038
  {renderColorField(
2034
- "背景色",
2039
+ t("panel.config.backgroundColor"),
2035
2040
  selectedElement.style?.backgroundColor ?? "",
2036
2041
  (next) => updateSelectedStyle({ backgroundColor: next || undefined })
2037
2042
  )}
2038
2043
  <label className="block space-y-1">
2039
- <div>背景图</div>
2044
+ <div>{t("panel.config.backgroundImage")}</div>
2040
2045
  <Input
2041
2046
  value={selectedElement.style?.backgroundImage ?? ""}
2042
2047
  onChange={(e) => updateSelectedStyle({ backgroundImage: e.target.value || undefined })}
@@ -2046,7 +2051,7 @@ export function PanelConfigSidebar({
2046
2051
  </label>
2047
2052
  <div className="flex items-center gap-2">
2048
2053
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
2049
- 上传图片
2054
+ {t("panel.config.uploadImage")}
2050
2055
  <Input
2051
2056
  type="file"
2052
2057
  accept="image/*"
@@ -2066,10 +2071,10 @@ export function PanelConfigSidebar({
2066
2071
  </>
2067
2072
  )}
2068
2073
  {renderFieldGroup(
2069
- "背景布局",
2074
+ t("panel.config.groupBgLayout"),
2070
2075
  <div className="grid grid-cols-2 gap-2">
2071
2076
  <label className="block space-y-1">
2072
- <div>背景尺寸</div>
2077
+ <div>{t("panel.config.backgroundSize")}</div>
2073
2078
  <Select
2074
2079
  value={selectedElement.style?.backgroundSize ?? "__none__"}
2075
2080
  onValueChange={(value) =>
@@ -2079,10 +2084,10 @@ export function PanelConfigSidebar({
2079
2084
  }
2080
2085
  >
2081
2086
  <SelectTrigger className="h-7">
2082
- <SelectValue placeholder="选择背景尺寸" />
2087
+ <SelectValue placeholder={t("panel.config.selectBackgroundSize")} />
2083
2088
  </SelectTrigger>
2084
2089
  <SelectContent>
2085
- <SelectItem value="__none__">默认</SelectItem>
2090
+ <SelectItem value="__none__">{t("common.default")}</SelectItem>
2086
2091
  <SelectItem value="cover">cover</SelectItem>
2087
2092
  <SelectItem value="contain">contain</SelectItem>
2088
2093
  <SelectItem value="100% 100%">100% 100%</SelectItem>
@@ -2091,7 +2096,7 @@ export function PanelConfigSidebar({
2091
2096
  </Select>
2092
2097
  </label>
2093
2098
  <label className="block space-y-1">
2094
- <div>背景位置</div>
2099
+ <div>{t("panel.config.backgroundPosition")}</div>
2095
2100
  <Select
2096
2101
  value={selectedElement.style?.backgroundPosition ?? "__none__"}
2097
2102
  onValueChange={(value) =>
@@ -2101,10 +2106,10 @@ export function PanelConfigSidebar({
2101
2106
  }
2102
2107
  >
2103
2108
  <SelectTrigger className="h-7">
2104
- <SelectValue placeholder="选择背景位置" />
2109
+ <SelectValue placeholder={t("panel.config.selectBackgroundPosition")} />
2105
2110
  </SelectTrigger>
2106
2111
  <SelectContent>
2107
- <SelectItem value="__none__">默认</SelectItem>
2112
+ <SelectItem value="__none__">{t("common.default")}</SelectItem>
2108
2113
  <SelectItem value="center">center</SelectItem>
2109
2114
  <SelectItem value="top">top</SelectItem>
2110
2115
  <SelectItem value="bottom">bottom</SelectItem>
@@ -2121,18 +2126,18 @@ export function PanelConfigSidebar({
2121
2126
  )}
2122
2127
  </>,
2123
2128
  true,
2124
- ["背景色", "背景图", "background", "backgroundSize", "backgroundPosition", "布局"]
2129
+ [t("panel.config.backgroundColor"), t("panel.config.backgroundImage"), "background", "backgroundSize", "backgroundPosition", t("panel.config.searchKwLayout")]
2125
2130
  )}
2126
2131
 
2127
2132
  {renderSection(
2128
2133
  "styleBorder",
2129
- "通用样式 / 边框",
2134
+ t("panel.config.sectionStyleBorder"),
2130
2135
  <>
2131
2136
  {renderFieldGroup(
2132
- "边框几何",
2137
+ t("panel.config.groupBorderGeometry"),
2133
2138
  <div className="grid grid-cols-2 gap-2">
2134
2139
  <label className="block space-y-1">
2135
- <div>边框宽度(px)</div>
2140
+ <div>{t("panel.config.borderWidth")}</div>
2136
2141
  <Input
2137
2142
  type="number"
2138
2143
  min={0}
@@ -2144,7 +2149,7 @@ export function PanelConfigSidebar({
2144
2149
  />
2145
2150
  </label>
2146
2151
  <label className="block space-y-1">
2147
- <div>边框圆角(px)</div>
2152
+ <div>{t("panel.config.borderRadius")}</div>
2148
2153
  <Input
2149
2154
  type="number"
2150
2155
  min={0}
@@ -2158,10 +2163,10 @@ export function PanelConfigSidebar({
2158
2163
  </div>
2159
2164
  )}
2160
2165
  {renderFieldGroup(
2161
- "边框视觉",
2166
+ t("panel.config.groupBorderVisual"),
2162
2167
  <div className="grid grid-cols-2 gap-2">
2163
2168
  <label className="block space-y-1">
2164
- <div>边框样式</div>
2169
+ <div>{t("panel.config.borderStyle")}</div>
2165
2170
  <Select
2166
2171
  value={selectedElement.style?.borderStyle ?? "solid"}
2167
2172
  onValueChange={(value) =>
@@ -2171,7 +2176,7 @@ export function PanelConfigSidebar({
2171
2176
  }
2172
2177
  >
2173
2178
  <SelectTrigger className="h-7">
2174
- <SelectValue placeholder="请选择边框样式" />
2179
+ <SelectValue placeholder={t("panel.config.selectBorderStyle")} />
2175
2180
  </SelectTrigger>
2176
2181
  <SelectContent>
2177
2182
  <SelectItem value="none">none</SelectItem>
@@ -2183,7 +2188,7 @@ export function PanelConfigSidebar({
2183
2188
  </Select>
2184
2189
  </label>
2185
2190
  {renderColorField(
2186
- "边框颜色",
2191
+ t("panel.config.borderColor"),
2187
2192
  selectedElement.style?.borderColor ?? "",
2188
2193
  (next) => updateSelectedStyle({ borderColor: next || undefined })
2189
2194
  )}
@@ -2191,20 +2196,20 @@ export function PanelConfigSidebar({
2191
2196
  )}
2192
2197
  </>,
2193
2198
  true,
2194
- ["边框", "border", "宽度", "圆角", "颜色", "样式"]
2199
+ [t("panel.config.searchKwBorder"), "border", t("panel.config.searchKwWidth"), t("panel.config.searchKwRadius"), t("panel.config.color"), t("panel.config.searchKwStyle")]
2195
2200
  )}
2196
2201
 
2197
2202
  {isChartElement ? (
2198
2203
  <>
2199
2204
  {renderSection(
2200
2205
  "chartBasic",
2201
- "图表配置 / 基础",
2206
+ t("panel.config.sectionChartBasic"),
2202
2207
  <>
2203
2208
  {renderFieldGroup(
2204
- "基础显示",
2209
+ t("panel.config.groupBasicDisplay"),
2205
2210
  <>
2206
- <label className="block space-y-1.5">
2207
- <div>标题</div>
2211
+ <label className="block space-y-1.5" data-config-field="chart.title">
2212
+ <div>{t("panel.config.title")}</div>
2208
2213
  <Input
2209
2214
  value={selectedElement.chart?.title ?? ""}
2210
2215
  onChange={(e) => updateSelectedChart({ title: e.target.value })}
@@ -2213,7 +2218,7 @@ export function PanelConfigSidebar({
2213
2218
  </label>
2214
2219
 
2215
2220
  {renderColorField(
2216
- "主色",
2221
+ t("panel.config.primaryColor"),
2217
2222
  selectedElement.chart?.color ?? "#3b82f6",
2218
2223
  (next) => updateSelectedChart({ color: next || "#3b82f6" })
2219
2224
  )}
@@ -2224,22 +2229,22 @@ export function PanelConfigSidebar({
2224
2229
  updateSelectedChart({ colorMode: checked ? "gradient" : "solid" })
2225
2230
  }
2226
2231
  />
2227
- <span>主色使用渐变</span>
2232
+ <span>{t("panel.config.usePrimaryGradient")}</span>
2228
2233
  </label>
2229
2234
  {selectedElement.chart?.colorMode === "gradient" ? (
2230
2235
  <div className="grid grid-cols-2 gap-2">
2231
2236
  {renderColorField(
2232
- "渐变起始色",
2237
+ t("panel.config.gradientFrom"),
2233
2238
  selectedElement.chart?.gradientFrom ?? selectedElement.chart?.color ?? "#3b82f6",
2234
2239
  (next) => updateSelectedChart({ gradientFrom: next || "#3b82f6" })
2235
2240
  )}
2236
2241
  {renderColorField(
2237
- "渐变结束色",
2242
+ t("panel.config.gradientTo"),
2238
2243
  selectedElement.chart?.gradientTo ?? "#22d3ee",
2239
2244
  (next) => updateSelectedChart({ gradientTo: next || "#22d3ee" })
2240
2245
  )}
2241
2246
  <label className="block space-y-1 col-span-2">
2242
- <div>渐变方向</div>
2247
+ <div>{t("panel.config.gradientDirection")}</div>
2243
2248
  <Select
2244
2249
  value={selectedElement.chart?.gradientDirection ?? "to-right"}
2245
2250
  onValueChange={(value) =>
@@ -2253,18 +2258,18 @@ export function PanelConfigSidebar({
2253
2258
  }
2254
2259
  >
2255
2260
  <SelectTrigger className="h-7">
2256
- <SelectValue placeholder="请选择渐变方向" />
2261
+ <SelectValue placeholder={t("panel.config.selectGradientDirection")} />
2257
2262
  </SelectTrigger>
2258
2263
  <SelectContent>
2259
- <SelectItem value="to-right">左 → 右</SelectItem>
2260
- <SelectItem value="to-bottom">上 → 下</SelectItem>
2261
- <SelectItem value="to-bottom-right">左上 → 右下</SelectItem>
2262
- <SelectItem value="to-top-right">左下 → 右上</SelectItem>
2264
+ <SelectItem value="to-right">{t("panel.config.dirToRight")}</SelectItem>
2265
+ <SelectItem value="to-bottom">{t("panel.config.dirToBottom")}</SelectItem>
2266
+ <SelectItem value="to-bottom-right">{t("panel.config.dirToBottomRight")}</SelectItem>
2267
+ <SelectItem value="to-top-right">{t("panel.config.dirToTopRight")}</SelectItem>
2263
2268
  </SelectContent>
2264
2269
  </Select>
2265
2270
  </label>
2266
2271
  <div className="col-span-2 space-y-1">
2267
- <div className="text-[11px] text-muted-foreground">渐变预览</div>
2272
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.gradientPreview")}</div>
2268
2273
  <div
2269
2274
  className="h-6 rounded border border-border/60"
2270
2275
  style={{
@@ -2283,7 +2288,7 @@ export function PanelConfigSidebar({
2283
2288
  </div>
2284
2289
  ) : null}
2285
2290
  <label className="block space-y-1.5">
2286
- <div>显示模式</div>
2291
+ <div>{t("panel.config.renderMode")}</div>
2287
2292
  <Select
2288
2293
  value={selectedElement.chart?.renderMode ?? "canvas"}
2289
2294
  onValueChange={(value) =>
@@ -2291,7 +2296,7 @@ export function PanelConfigSidebar({
2291
2296
  }
2292
2297
  >
2293
2298
  <SelectTrigger className="h-7">
2294
- <SelectValue placeholder="请选择显示模式" />
2299
+ <SelectValue placeholder={t("panel.config.selectRenderMode")} />
2295
2300
  </SelectTrigger>
2296
2301
  <SelectContent>
2297
2302
  <SelectItem value="canvas">Canvas</SelectItem>
@@ -2302,7 +2307,7 @@ export function PanelConfigSidebar({
2302
2307
  </>
2303
2308
  )}
2304
2309
  {renderFieldGroup(
2305
- "提示框 Tooltip",
2310
+ t("panel.config.groupTooltip"),
2306
2311
  <>
2307
2312
  <div className="grid grid-cols-2 gap-2">
2308
2313
  <label className="flex items-center gap-2">
@@ -2313,10 +2318,10 @@ export function PanelConfigSidebar({
2313
2318
  updateSelectedChart({ tooltipShow: checked === true })
2314
2319
  }
2315
2320
  />
2316
- <span>显示 Tooltip</span>
2321
+ <span>{t("panel.config.showTooltip")}</span>
2317
2322
  </label>
2318
2323
  <label className="block space-y-1">
2319
- <div>Tooltip 触发方式</div>
2324
+ <div>{t("panel.config.tooltipTrigger")}</div>
2320
2325
  <Select
2321
2326
  value={selectedElement.chart?.tooltipTrigger ?? "axis"}
2322
2327
  onValueChange={(value) =>
@@ -2324,7 +2329,7 @@ export function PanelConfigSidebar({
2324
2329
  }
2325
2330
  >
2326
2331
  <SelectTrigger className="h-7">
2327
- <SelectValue placeholder="请选择触发方式" />
2332
+ <SelectValue placeholder={t("panel.config.selectTooltipTrigger")} />
2328
2333
  </SelectTrigger>
2329
2334
  <SelectContent>
2330
2335
  <SelectItem value="axis">axis</SelectItem>
@@ -2335,42 +2340,39 @@ export function PanelConfigSidebar({
2335
2340
  </div>
2336
2341
  <div className="grid grid-cols-2 gap-2">
2337
2342
  {renderColorField(
2338
- "Tooltip 背景色",
2343
+ t("panel.config.tooltipBgColor"),
2339
2344
  selectedElement.chart?.tooltipBackgroundColor ?? "#0f172a",
2340
2345
  (next) => updateSelectedChart({ tooltipBackgroundColor: next || "#0f172a" })
2341
2346
  )}
2342
2347
  {renderColorField(
2343
- "Tooltip 文字色",
2348
+ t("panel.config.tooltipTextColor"),
2344
2349
  selectedElement.chart?.tooltipTextColor ?? "#f8fafc",
2345
2350
  (next) => updateSelectedChart({ tooltipTextColor: next || "#f8fafc" })
2346
2351
  )}
2347
2352
  </div>
2348
2353
  <label className="block space-y-1">
2349
- {renderFormatterLabel("Tooltip Formatter(可选)")}
2354
+ {renderFormatterLabel(t("panel.config.tooltipFormatter"))}
2350
2355
  <Input
2351
2356
  value={selectedElement.chart?.tooltipFormatter ?? ""}
2352
2357
  onChange={(e) =>
2353
2358
  updateSelectedChart({ tooltipFormatter: e.target.value || undefined })
2354
2359
  }
2355
- placeholder="例如:{b}: {c}"
2360
+ placeholder={t("panel.config.tooltipFormatterPlaceholder")}
2356
2361
  className="h-7"
2357
2362
  />
2358
2363
  </label>
2359
2364
  </>
2360
2365
  )}
2361
2366
  {renderFieldGroup(
2362
- "数据",
2367
+ t("panel.config.groupData"),
2363
2368
  <>
2364
2369
  <label className="block space-y-1">
2365
- <div>类目(逗号分隔)</div>
2370
+ <div>{t("panel.config.labelsCsv")}</div>
2366
2371
  <Input
2367
- value={(selectedElement.chart?.labels ?? []).join(",")}
2372
+ value={getChartLabelsDisplayText(selectedElement.chart)}
2368
2373
  onChange={(e) =>
2369
2374
  updateSelectedChart({
2370
- labels: e.target.value
2371
- .split(",")
2372
- .map((s) => s.trim())
2373
- .filter(Boolean),
2375
+ labelsText: e.target.value,
2374
2376
  })
2375
2377
  }
2376
2378
  className="h-7"
@@ -2378,7 +2380,7 @@ export function PanelConfigSidebar({
2378
2380
  </label>
2379
2381
 
2380
2382
  <label className="block space-y-1">
2381
- <div>数值(逗号分隔)</div>
2383
+ <div>{t("panel.config.valuesCsv")}</div>
2382
2384
  <Input
2383
2385
  value={getChartValuesDisplayText(selectedElement.chart)}
2384
2386
  onChange={(e) =>
@@ -2397,24 +2399,24 @@ export function PanelConfigSidebar({
2397
2399
  selectedChartType === "area" ||
2398
2400
  selectedChartType === "scatter" ? (
2399
2401
  renderFieldGroup(
2400
- "坐标轴",
2402
+ t("panel.config.groupAxes"),
2401
2403
  <>
2402
2404
  <div className="grid grid-cols-2 gap-2">
2403
2405
  <label className="block">
2404
- <div className="mb-1">X 轴名称</div>
2406
+ <div className="mb-1">{t("panel.config.xAxisName")}</div>
2405
2407
  <Input
2406
2408
  value={selectedElement.chart?.xAxisName ?? ""}
2407
2409
  onChange={(e) => updateSelectedChart({ xAxisName: e.target.value })}
2408
- placeholder="例如:日期 / 类目"
2410
+ placeholder={t("panel.config.xAxisNamePlaceholder")}
2409
2411
  className="h-7"
2410
2412
  />
2411
2413
  </label>
2412
2414
  <label className="block">
2413
- <div className="mb-1">Y 轴名称</div>
2415
+ <div className="mb-1">{t("panel.config.yAxisName")}</div>
2414
2416
  <Input
2415
2417
  value={selectedElement.chart?.yAxisName ?? ""}
2416
2418
  onChange={(e) => updateSelectedChart({ yAxisName: e.target.value })}
2417
- placeholder="例如:销量 / 数值"
2419
+ placeholder={t("panel.config.yAxisNamePlaceholder")}
2418
2420
  className="h-7"
2419
2421
  />
2420
2422
  </label>
@@ -2428,7 +2430,7 @@ export function PanelConfigSidebar({
2428
2430
  updateSelectedChart({ xAxisTickShow: checked === true })
2429
2431
  }
2430
2432
  />
2431
- <span>X 轴刻度线</span>
2433
+ <span>{t("panel.config.xAxisTick")}</span>
2432
2434
  </label>
2433
2435
  <label className="flex items-center gap-2">
2434
2436
  <Checkbox
@@ -2438,17 +2440,17 @@ export function PanelConfigSidebar({
2438
2440
  updateSelectedChart({ yAxisTickShow: checked === true })
2439
2441
  }
2440
2442
  />
2441
- <span>Y 轴刻度线</span>
2443
+ <span>{t("panel.config.yAxisTick")}</span>
2442
2444
  </label>
2443
2445
  </div>
2444
2446
  <div className="grid grid-cols-2 gap-2">
2445
2447
  {renderColorField(
2446
- "X 轴刻度线颜色",
2448
+ t("panel.config.xAxisTickColor"),
2447
2449
  selectedElement.chart?.xAxisTickColor ?? "#94a3b8",
2448
2450
  (next) => updateSelectedChart({ xAxisTickColor: next || "#94a3b8" })
2449
2451
  )}
2450
2452
  {renderColorField(
2451
- "Y 轴刻度线颜色",
2453
+ t("panel.config.yAxisTickColor"),
2452
2454
  selectedElement.chart?.yAxisTickColor ?? "#94a3b8",
2453
2455
  (next) => updateSelectedChart({ yAxisTickColor: next || "#94a3b8" })
2454
2456
  )}
@@ -2462,7 +2464,7 @@ export function PanelConfigSidebar({
2462
2464
  updateSelectedChart({ xAxisSplitLineShow: checked === true })
2463
2465
  }
2464
2466
  />
2465
- <span>X 轴分割线</span>
2467
+ <span>{t("panel.config.xAxisSplitLine")}</span>
2466
2468
  </label>
2467
2469
  <label className="flex items-center gap-2">
2468
2470
  <Checkbox
@@ -2472,36 +2474,36 @@ export function PanelConfigSidebar({
2472
2474
  updateSelectedChart({ yAxisSplitLineShow: checked === true })
2473
2475
  }
2474
2476
  />
2475
- <span>Y 轴分割线</span>
2477
+ <span>{t("panel.config.yAxisSplitLine")}</span>
2476
2478
  </label>
2477
2479
  </div>
2478
2480
  <div className="grid grid-cols-2 gap-2">
2479
2481
  {renderColorField(
2480
- "X 轴分割线颜色",
2482
+ t("panel.config.xAxisSplitLineColor"),
2481
2483
  selectedElement.chart?.xAxisSplitLineColor ?? "#e2e8f0",
2482
2484
  (next) => updateSelectedChart({ xAxisSplitLineColor: next || "#e2e8f0" })
2483
2485
  )}
2484
2486
  {renderColorField(
2485
- "Y 轴分割线颜色",
2487
+ t("panel.config.yAxisSplitLineColor"),
2486
2488
  selectedElement.chart?.yAxisSplitLineColor ?? "#e2e8f0",
2487
2489
  (next) => updateSelectedChart({ yAxisSplitLineColor: next || "#e2e8f0" })
2488
2490
  )}
2489
2491
  </div>
2490
2492
  <div className="grid grid-cols-2 gap-2">
2491
2493
  {renderColorField(
2492
- "X 轴标签颜色",
2494
+ t("panel.config.xAxisLabelColor"),
2493
2495
  selectedElement.chart?.xAxisLabelColor ?? "#64748b",
2494
2496
  (next) => updateSelectedChart({ xAxisLabelColor: next || "#64748b" })
2495
2497
  )}
2496
2498
  {renderColorField(
2497
- "Y 轴标签颜色",
2499
+ t("panel.config.yAxisLabelColor"),
2498
2500
  selectedElement.chart?.yAxisLabelColor ?? "#64748b",
2499
2501
  (next) => updateSelectedChart({ yAxisLabelColor: next || "#64748b" })
2500
2502
  )}
2501
2503
  </div>
2502
2504
  <div className="grid grid-cols-2 gap-2">
2503
2505
  <label className="block">
2504
- <div className="mb-1">X 轴标签字号</div>
2506
+ <div className="mb-1">{t("panel.config.xAxisLabelFontSize")}</div>
2505
2507
  <Input
2506
2508
  type="number"
2507
2509
  min={8}
@@ -2516,7 +2518,7 @@ export function PanelConfigSidebar({
2516
2518
  />
2517
2519
  </label>
2518
2520
  <label className="block">
2519
- <div className="mb-1">Y 轴标签字号</div>
2521
+ <div className="mb-1">{t("panel.config.yAxisLabelFontSize")}</div>
2520
2522
  <Input
2521
2523
  type="number"
2522
2524
  min={8}
@@ -2540,7 +2542,7 @@ export function PanelConfigSidebar({
2540
2542
  updateSelectedChart({ xAxisLabelAutoEllipsis: checked === true })
2541
2543
  }
2542
2544
  />
2543
- <span>X 轴标签自动缩略</span>
2545
+ <span>{t("panel.config.xAxisLabelAutoEllipsis")}</span>
2544
2546
  </label>
2545
2547
  <label className="flex items-center gap-2">
2546
2548
  <Checkbox
@@ -2550,7 +2552,7 @@ export function PanelConfigSidebar({
2550
2552
  updateSelectedChart({ yAxisLabelAutoEllipsis: checked === true })
2551
2553
  }
2552
2554
  />
2553
- <span>Y 轴标签自动缩略</span>
2555
+ <span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
2554
2556
  </label>
2555
2557
  </div>
2556
2558
  </>
@@ -2562,11 +2564,11 @@ export function PanelConfigSidebar({
2562
2564
  selectedChartType === "area" ||
2563
2565
  selectedChartType === "pie" ? (
2564
2566
  renderFieldGroup(
2565
- "系列",
2567
+ t("panel.config.groupSeries"),
2566
2568
  <>
2567
2569
  {selectedChartType === "bar" ? (
2568
2570
  <label className="block space-y-1">
2569
- <div>柱宽(px)</div>
2571
+ <div>{t("panel.config.barWidthPx")}</div>
2570
2572
  <Input
2571
2573
  type="number"
2572
2574
  min={1}
@@ -2588,14 +2590,14 @@ export function PanelConfigSidebar({
2588
2590
  updateSelectedChart({ smooth: checked === true })
2589
2591
  }
2590
2592
  />
2591
- <span>平滑曲线</span>
2593
+ <span>{t("panel.config.smooth")}</span>
2592
2594
  </label>
2593
2595
  ) : null}
2594
2596
 
2595
2597
  {selectedChartType === "pie" ? (
2596
2598
  <div className="grid grid-cols-2 gap-2">
2597
2599
  <label className="block space-y-1">
2598
- <div>内半径(%)</div>
2600
+ <div>{t("panel.config.pieInnerRadiusPct")}</div>
2599
2601
  <Input
2600
2602
  type="number"
2601
2603
  min={0}
@@ -2613,7 +2615,7 @@ export function PanelConfigSidebar({
2613
2615
  />
2614
2616
  </label>
2615
2617
  <label className="block space-y-1">
2616
- <div>外半径(%)</div>
2618
+ <div>{t("panel.config.pieOuterRadiusPct")}</div>
2617
2619
  <Input
2618
2620
  type="number"
2619
2621
  min={1}
@@ -2638,15 +2640,15 @@ export function PanelConfigSidebar({
2638
2640
  </>,
2639
2641
  true,
2640
2642
  [
2641
- "图表",
2643
+ t("panel.config.searchKwChart"),
2642
2644
  "title",
2643
2645
  "tooltip",
2644
- "x轴",
2645
- "y轴",
2646
+ t("panel.config.searchKwXAxis"),
2647
+ t("panel.config.searchKwYAxis"),
2646
2648
  "axis",
2647
2649
  "label",
2648
- "刻度线",
2649
- "分割线",
2650
+ t("panel.config.searchKwTick"),
2651
+ t("panel.config.searchKwSplit"),
2650
2652
  "render",
2651
2653
  "svg",
2652
2654
  "canvas",
@@ -2655,10 +2657,10 @@ export function PanelConfigSidebar({
2655
2657
 
2656
2658
  {renderSection(
2657
2659
  "chartAdvanced",
2658
- "图表配置 / 高级",
2660
+ t("panel.config.sectionChartAdvanced"),
2659
2661
  <>
2660
2662
  {renderFieldGroup(
2661
- "常用项(表单)",
2663
+ t("panel.config.groupCommonForm"),
2662
2664
  <div className="grid grid-cols-1 gap-2">
2663
2665
  <div className="rounded-lg border border-border/60 bg-background/70 p-3">
2664
2666
  <Collapsible
@@ -2670,75 +2672,75 @@ export function PanelConfigSidebar({
2670
2672
  <button
2671
2673
  type="button"
2672
2674
  className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
2673
- aria-label={isSectionExpanded("chartAdvancedLayout", false) ? "收起布局与坐标" : "展开布局与坐标"}
2675
+ aria-label={isSectionExpanded("chartAdvancedLayout", false) ? t("panel.config.collapseLayoutCoord") : t("panel.config.expandLayoutCoord")}
2674
2676
  >
2675
2677
  {isSectionExpanded("chartAdvancedLayout", false) ? "▾" : "▸"}
2676
2678
  </button>
2677
2679
  </CollapsibleTrigger>
2678
- <div className="text-[11px] font-medium text-muted-foreground">布局与坐标</div>
2680
+ <div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupLayoutAndCoord")}</div>
2679
2681
  </div>
2680
2682
  <CollapsibleContent>
2681
2683
  <div className="grid grid-cols-2 gap-2">
2682
2684
  <label className="block space-y-1.5">
2683
- <div className={optionLabelTextClass}>网格左距(grid.left)</div>
2685
+ <div className={optionLabelTextClass}>{t("panel.config.gridLeftForm")}</div>
2684
2686
  <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.left ?? 28)} onChange={(e) => updateSelectedOptionForm({ grid: { left: Number(e.target.value) || 0 } })} />
2685
2687
  </label>
2686
2688
  <label className="block space-y-1.5">
2687
- <div className={optionLabelTextClass}>网格右距(grid.right)</div>
2689
+ <div className={optionLabelTextClass}>{t("panel.config.gridRightForm")}</div>
2688
2690
  <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.right ?? 10)} onChange={(e) => updateSelectedOptionForm({ grid: { right: Number(e.target.value) || 0 } })} />
2689
2691
  </label>
2690
2692
  <label className="block space-y-1.5">
2691
- <div className={optionLabelTextClass}>网格上距(grid.top)</div>
2693
+ <div className={optionLabelTextClass}>{t("panel.config.gridTopForm")}</div>
2692
2694
  <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.top ?? 30)} onChange={(e) => updateSelectedOptionForm({ grid: { top: Number(e.target.value) || 0 } })} />
2693
2695
  </label>
2694
2696
  <label className="block space-y-1.5">
2695
- <div className={optionLabelTextClass}>网格下距(grid.bottom)</div>
2697
+ <div className={optionLabelTextClass}>{t("panel.config.gridBottomForm")}</div>
2696
2698
  <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.bottom ?? 20)} onChange={(e) => updateSelectedOptionForm({ grid: { bottom: Number(e.target.value) || 0 } })} />
2697
2699
  </label>
2698
2700
  <label className="block space-y-1.5">
2699
- <div className={optionLabelTextClass}>图例位置(legend.top)</div>
2701
+ <div className={optionLabelTextClass}>{t("panel.config.legendPositionForm")}</div>
2700
2702
  <Select value={String((selectedElement.chart?.option as any)?.legend?.top ?? "top")} onValueChange={(value) => updateSelectedOptionForm({ legend: { top: value } })}>
2701
- <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder="选择图例位置" /></SelectTrigger>
2703
+ <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectLegendPosition")} /></SelectTrigger>
2702
2704
  <SelectContent>
2703
- <SelectItem value="top">顶部</SelectItem>
2704
- <SelectItem value="bottom">底部</SelectItem>
2705
- <SelectItem value="left">左侧</SelectItem>
2706
- <SelectItem value="right">右侧</SelectItem>
2705
+ <SelectItem value="top">{t("panel.config.legendTop")}</SelectItem>
2706
+ <SelectItem value="bottom">{t("panel.config.legendBottom")}</SelectItem>
2707
+ <SelectItem value="left">{t("panel.config.legendLeft")}</SelectItem>
2708
+ <SelectItem value="right">{t("panel.config.legendRight")}</SelectItem>
2707
2709
  </SelectContent>
2708
2710
  </Select>
2709
2711
  </label>
2710
2712
  <label className="block space-y-1.5">
2711
- <div className={optionLabelTextClass}>图例排列(legend.orient)</div>
2713
+ <div className={optionLabelTextClass}>{t("panel.config.legendOrientForm")}</div>
2712
2714
  <Select value={String((selectedElement.chart?.option as any)?.legend?.orient ?? "horizontal")} onValueChange={(value) => updateSelectedOptionForm({ legend: { orient: value } })}>
2713
- <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder="选择图例排列" /></SelectTrigger>
2715
+ <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectLegendOrient")} /></SelectTrigger>
2714
2716
  <SelectContent>
2715
- <SelectItem value="horizontal">横向</SelectItem>
2716
- <SelectItem value="vertical">纵向</SelectItem>
2717
+ <SelectItem value="horizontal">{t("panel.config.legendHorizontal")}</SelectItem>
2718
+ <SelectItem value="vertical">{t("panel.config.legendVertical")}</SelectItem>
2717
2719
  </SelectContent>
2718
2720
  </Select>
2719
2721
  </label>
2720
2722
  <label className="block space-y-1.5">
2721
- {renderOptionLabel("X 轴最小值", "xAxis.min", "设置横轴最小值,留空可在 JSON 里删除。")}
2723
+ {renderOptionLabel(t("panel.config.xAxisMin"), "xAxis.min", t("panel.config.xAxisMinHint"))}
2722
2724
  <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.min ?? 0)} onChange={(e) => updateSelectedOptionForm({ xAxis: { min: Number(e.target.value) || 0 } })} />
2723
2725
  </label>
2724
2726
  <label className="block space-y-1.5">
2725
- {renderOptionLabel("X 轴最大值", "xAxis.max", "设置横轴最大值,留空可在 JSON 里删除。")}
2727
+ {renderOptionLabel(t("panel.config.xAxisMax"), "xAxis.max", t("panel.config.xAxisMaxHint"))}
2726
2728
  <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.max ?? 100)} onChange={(e) => updateSelectedOptionForm({ xAxis: { max: Number(e.target.value) || 0 } })} />
2727
2729
  </label>
2728
2730
  <label className="block space-y-1.5">
2729
- {renderOptionLabel("Y 轴最小值", "yAxis.min", "设置纵轴最小值,留空可在 JSON 里删除。")}
2731
+ {renderOptionLabel(t("panel.config.yAxisMin"), "yAxis.min", t("panel.config.yAxisMinHint"))}
2730
2732
  <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.min ?? 0)} onChange={(e) => updateSelectedOptionForm({ yAxis: { min: Number(e.target.value) || 0 } })} />
2731
2733
  </label>
2732
2734
  <label className="block space-y-1.5">
2733
- {renderOptionLabel("Y 轴最大值", "yAxis.max", "设置纵轴最大值,留空可在 JSON 里删除。")}
2735
+ {renderOptionLabel(t("panel.config.yAxisMax"), "yAxis.max", t("panel.config.yAxisMaxHint"))}
2734
2736
  <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.max ?? 100)} onChange={(e) => updateSelectedOptionForm({ yAxis: { max: Number(e.target.value) || 0 } })} />
2735
2737
  </label>
2736
2738
  <label className="block space-y-1.5">
2737
- {renderOptionLabel("X 轴标签旋转", "xAxis.axisLabel.rotate", "单位为度。")}
2739
+ {renderOptionLabel(t("panel.config.xAxisLabelRotate"), "xAxis.axisLabel.rotate", t("panel.config.rotateUnitHint"))}
2738
2740
  <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.axisLabel?.rotate ?? 0)} onChange={(e) => updateSelectedOptionForm({ xAxis: { axisLabel: { rotate: Number(e.target.value) || 0 } } })} />
2739
2741
  </label>
2740
2742
  <label className="block space-y-1.5">
2741
- {renderOptionLabel("Y 轴标签旋转", "yAxis.axisLabel.rotate", "单位为度。")}
2743
+ {renderOptionLabel(t("panel.config.yAxisLabelRotate"), "yAxis.axisLabel.rotate", t("panel.config.rotateUnitHint"))}
2742
2744
  <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.axisLabel?.rotate ?? 0)} onChange={(e) => updateSelectedOptionForm({ yAxis: { axisLabel: { rotate: Number(e.target.value) || 0 } } })} />
2743
2745
  </label>
2744
2746
  </div>
@@ -2756,37 +2758,37 @@ export function PanelConfigSidebar({
2756
2758
  <button
2757
2759
  type="button"
2758
2760
  className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
2759
- aria-label={isSectionExpanded("chartAdvancedHighFreq", true) ? "收起高频项" : "展开高频项"}
2761
+ aria-label={isSectionExpanded("chartAdvancedHighFreq", true) ? t("panel.config.collapseHighFreq") : t("panel.config.expandHighFreq")}
2760
2762
  >
2761
2763
  {isSectionExpanded("chartAdvancedHighFreq", true) ? "▾" : "▸"}
2762
2764
  </button>
2763
2765
  </CollapsibleTrigger>
2764
- <div className="text-[11px] font-medium text-muted-foreground">高频项</div>
2766
+ <div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupHighFreq")}</div>
2765
2767
  </div>
2766
2768
  <CollapsibleContent>
2767
2769
  <div className="grid grid-cols-1 gap-2">
2768
2770
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2769
2771
  <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.legend?.show ?? true)} onCheckedChange={(checked) => updateSelectedOptionForm({ legend: { show: checked === true } })} />
2770
- {renderOptionLabel("显示图例", "legend.show", "控制图例显隐。")}
2772
+ {renderOptionLabel(t("panel.config.showLegend"), "legend.show", t("panel.config.showLegendHint"))}
2771
2773
  </label>
2772
2774
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2773
2775
  <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.grid?.containLabel ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ grid: { containLabel: checked === true } })} />
2774
- {renderOptionLabel("网格包含标签", "grid.containLabel", "自动为坐标轴标签预留空间。")}
2776
+ {renderOptionLabel(t("panel.config.gridContainLabel"), "grid.containLabel", t("panel.config.gridContainLabelHint"))}
2775
2777
  </label>
2776
2778
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2777
2779
  <Checkbox className={optionCheckboxClass} checked={Boolean(Array.isArray((selectedElement.chart?.option as any)?.dataZoom) && (selectedElement.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "inside"))} onCheckedChange={(checked) => { const prev = Array.isArray((selectedElement.chart?.option as any)?.dataZoom) ? [...(selectedElement.chart?.option as any).dataZoom] : []; const next = checked ? [...prev.filter((z: any) => z?.type !== "inside"), { type: "inside" }] : prev.filter((z: any) => z?.type !== "inside"); updateSelectedOptionForm({ dataZoom: next }); }} />
2778
- {renderOptionLabel("内置缩放", "dataZoom[type=inside]", "启用鼠标滚轮/手势缩放。")}
2780
+ {renderOptionLabel(t("panel.config.zoomInside"), "dataZoom[type=inside]", t("panel.config.zoomInsideHint"))}
2779
2781
  </label>
2780
2782
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2781
2783
  <Checkbox className={optionCheckboxClass} checked={Boolean(Array.isArray((selectedElement.chart?.option as any)?.dataZoom) && (selectedElement.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "slider"))} onCheckedChange={(checked) => { const prev = Array.isArray((selectedElement.chart?.option as any)?.dataZoom) ? [...(selectedElement.chart?.option as any).dataZoom] : []; const next = checked ? [...prev.filter((z: any) => z?.type !== "slider"), { type: "slider" }] : prev.filter((z: any) => z?.type !== "slider"); updateSelectedOptionForm({ dataZoom: next }); }} />
2782
- {renderOptionLabel("滑块缩放", "dataZoom[type=slider]", "显示底部拖拽缩放条。")}
2784
+ {renderOptionLabel(t("panel.config.zoomSlider"), "dataZoom[type=slider]", t("panel.config.zoomSliderHint"))}
2783
2785
  </label>
2784
2786
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2785
2787
  <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.animation ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ animation: checked === true })} />
2786
- {renderOptionLabel("开启动画", "animation", "开启后切换数据时有过渡动效。")}
2788
+ {renderOptionLabel(t("panel.config.enableAnimation"), "animation", t("panel.config.enableAnimationHint"))}
2787
2789
  </label>
2788
2790
  <label className="block space-y-1.5">
2789
- {renderOptionLabel("动画时长", "animationDuration", "动画持续时间,单位毫秒。")}
2791
+ {renderOptionLabel(t("panel.config.animationDuration"), "animationDuration", t("panel.config.animationDurationHint"))}
2790
2792
  <Input type="number" min={0} className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.animationDuration ?? 300)} onChange={(e) => updateSelectedOptionForm({ animationDuration: Math.max(0, Number(e.target.value) || 0) })} />
2791
2793
  </label>
2792
2794
  </div>
@@ -2801,33 +2803,33 @@ export function PanelConfigSidebar({
2801
2803
  <button
2802
2804
  type="button"
2803
2805
  className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
2804
- aria-label={isSectionExpanded("chartAdvancedAxisPointer", false) ? "收起轴指示器项" : "展开轴指示器项"}
2806
+ aria-label={isSectionExpanded("chartAdvancedAxisPointer", false) ? t("panel.config.collapseAxisPointer") : t("panel.config.expandAxisPointer")}
2805
2807
  >
2806
2808
  {isSectionExpanded("chartAdvancedAxisPointer", false) ? "▾" : "▸"}
2807
2809
  </button>
2808
2810
  </CollapsibleTrigger>
2809
- <div className="text-[11px] font-medium text-muted-foreground">轴指示器与对齐</div>
2811
+ <div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupAxisPointer")}</div>
2810
2812
  </div>
2811
2813
  <CollapsibleContent>
2812
2814
  <div className="grid grid-cols-1 gap-2">
2813
2815
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2814
2816
  <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.show ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { show: checked === true } })} />
2815
- {renderOptionLabel("显示轴指示器", "axisPointer.show", "悬停时显示轴对齐提示。")}
2817
+ {renderOptionLabel(t("panel.config.showAxisPointer"), "axisPointer.show", t("panel.config.showAxisPointerHint"))}
2816
2818
  </label>
2817
2819
  <label className="block space-y-1.5 rounded-md bg-muted/30 px-2 py-1.5">
2818
- {renderOptionLabel("轴指示器类型", "axisPointer.type", "设置指示器样式:线/阴影/十字。")}
2820
+ {renderOptionLabel(t("panel.config.axisPointerType"), "axisPointer.type", t("panel.config.axisPointerTypeHint"))}
2819
2821
  <Select value={String((selectedElement.chart?.option as any)?.axisPointer?.type ?? "line")} onValueChange={(value) => updateSelectedOptionForm({ axisPointer: { type: value } })}>
2820
- <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder="选择轴指示器类型" /></SelectTrigger>
2822
+ <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectAxisPointerType")} /></SelectTrigger>
2821
2823
  <SelectContent>
2822
- <SelectItem value="line">线</SelectItem>
2823
- <SelectItem value="shadow">阴影</SelectItem>
2824
- <SelectItem value="cross">十字</SelectItem>
2824
+ <SelectItem value="line">{t("panel.config.axisPointerLine")}</SelectItem>
2825
+ <SelectItem value="shadow">{t("panel.config.axisPointerShadow")}</SelectItem>
2826
+ <SelectItem value="cross">{t("panel.config.axisPointerCross")}</SelectItem>
2825
2827
  </SelectContent>
2826
2828
  </Select>
2827
2829
  </label>
2828
2830
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2829
2831
  <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.snap ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { snap: checked === true } })} />
2830
- {renderOptionLabel("轴指示器吸附", "axisPointer.snap", "指示器吸附到最近数据点。")}
2832
+ {renderOptionLabel(t("panel.config.axisPointerSnap"), "axisPointer.snap", t("panel.config.axisPointerSnapHint"))}
2831
2833
  </label>
2832
2834
  </div>
2833
2835
  </CollapsibleContent>
@@ -2837,7 +2839,7 @@ export function PanelConfigSidebar({
2837
2839
  </div>
2838
2840
  )}
2839
2841
  {renderFieldGroup(
2840
- "JSON 高级模式",
2842
+ t("panel.config.groupJsonAdvanced"),
2841
2843
  <>
2842
2844
  <label className="flex items-center gap-2 rounded-md bg-background/70 px-2 py-1.5">
2843
2845
  <Checkbox
@@ -2845,11 +2847,11 @@ export function PanelConfigSidebar({
2845
2847
  className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
2846
2848
  onCheckedChange={(checked) => setIsAdvancedOptionMode(checked === true)}
2847
2849
  />
2848
- <span>开启高级模式(直接编辑图表 JSON 配置)</span>
2850
+ <span>{t("panel.config.enableAdvancedJson")}</span>
2851
+ <ConfigHintIcon label={t("panel.config.advancedJsonHintLabel")}>
2852
+ {t("panel.config.advancedJsonHint")}
2853
+ </ConfigHintIcon>
2849
2854
  </label>
2850
- <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
2851
- 基础配置会先生成图表配置,高级模式会在此基础上覆盖(深度合并)。
2852
- </div>
2853
2855
  {isAdvancedOptionMode ? (
2854
2856
  <>
2855
2857
  <Textarea
@@ -2862,7 +2864,7 @@ export function PanelConfigSidebar({
2862
2864
  updateSelectedChart({ option: parsed });
2863
2865
  setOptionJsonError(null);
2864
2866
  } catch {
2865
- setOptionJsonError("JSON 格式错误,修正后会自动应用");
2867
+ setOptionJsonError(t("panel.config.jsonInvalid"));
2866
2868
  }
2867
2869
  }}
2868
2870
  spellCheck={false}
@@ -2874,7 +2876,7 @@ export function PanelConfigSidebar({
2874
2876
  </div>
2875
2877
  ) : (
2876
2878
  <div className="rounded border border-emerald-500/30 bg-emerald-500/10 px-2 py-1.5 text-[11px] text-emerald-700 dark:text-emerald-300">
2877
- JSON 有效,已实时应用到当前图表。
2879
+ {t("panel.config.jsonValidApplied")}
2878
2880
  </div>
2879
2881
  )}
2880
2882
  </>
@@ -2883,16 +2885,35 @@ export function PanelConfigSidebar({
2883
2885
  )}
2884
2886
  </>,
2885
2887
  false,
2886
- ["json", "option", "高级", "echarts"]
2888
+ ["json", "option", t("panel.config.searchKwAdvanced"), "echarts"]
2887
2889
  )}
2888
2890
  </>
2891
+ ) : selectedElement.materialType === "table" ? (
2892
+ renderSection(
2893
+ "tableConfig",
2894
+ t("panel.config.sectionTable"),
2895
+ <PanelConfigTableSection
2896
+ element={selectedElement}
2897
+ disabled={!isNodeEditable}
2898
+ updateElement={updateElement}
2899
+ blueprintNodeOptions={blueprintNodeOptions}
2900
+ />,
2901
+ true,
2902
+ [
2903
+ t("panel.material.table"),
2904
+ "table",
2905
+ t("panel.config.tableSource"),
2906
+ t("panel.config.tableColumns"),
2907
+ t("panel.config.tableRowsText"),
2908
+ ]
2909
+ )
2889
2910
  ) : selectedElement.materialType === "text" ? (
2890
2911
  renderSection(
2891
2912
  "textConfig",
2892
- "文本配置",
2913
+ t("panel.config.sectionText"),
2893
2914
  <>
2894
2915
  {renderFieldGroup(
2895
- "文本内容",
2916
+ t("panel.config.groupTextContent"),
2896
2917
  <>
2897
2918
  <div className="flex items-center gap-1">
2898
2919
  <button
@@ -2919,6 +2940,7 @@ export function PanelConfigSidebar({
2919
2940
  </div>
2920
2941
  <div
2921
2942
  ref={textEditorRef}
2943
+ data-config-field="textHtml"
2922
2944
  className="min-h-[120px] rounded border border-border bg-background px-2 py-1.5 text-xs leading-6 outline-none"
2923
2945
  style={{
2924
2946
  fontFamily: selectedElement.textFontFamily || undefined,
@@ -2944,10 +2966,10 @@ export function PanelConfigSidebar({
2944
2966
  </>
2945
2967
  )}
2946
2968
  {renderFieldGroup(
2947
- "文字样式",
2969
+ t("panel.config.groupTextStyle"),
2948
2970
  <>
2949
2971
  <label className="block space-y-1">
2950
- <div>字体</div>
2972
+ <div>{t("panel.config.fontFamily")}</div>
2951
2973
  <Input
2952
2974
  value={selectedElement.textFontFamily ?? ""}
2953
2975
  onChange={(e) =>
@@ -2955,13 +2977,13 @@ export function PanelConfigSidebar({
2955
2977
  textFontFamily: e.target.value || undefined,
2956
2978
  })
2957
2979
  }
2958
- placeholder="如:Inter, PingFang SC, Microsoft YaHei"
2980
+ placeholder={t("panel.config.fontFamilyPlaceholder")}
2959
2981
  className="h-7"
2960
2982
  />
2961
2983
  </label>
2962
2984
  <div className="grid grid-cols-2 gap-2">
2963
2985
  <label className="block space-y-1">
2964
- <div>字号(px)</div>
2986
+ <div>{t("panel.config.fontSizePx")}</div>
2965
2987
  <Input
2966
2988
  type="number"
2967
2989
  min={8}
@@ -2977,7 +2999,7 @@ export function PanelConfigSidebar({
2977
2999
  />
2978
3000
  </label>
2979
3001
  <label className="block space-y-1">
2980
- <div>字重</div>
3002
+ <div>{t("panel.config.fontWeight")}</div>
2981
3003
  <Select
2982
3004
  value={selectedElement.textFontWeight ?? "400"}
2983
3005
  onValueChange={(value) =>
@@ -2987,7 +3009,7 @@ export function PanelConfigSidebar({
2987
3009
  }
2988
3010
  >
2989
3011
  <SelectTrigger className="h-7">
2990
- <SelectValue placeholder="选择字重" />
3012
+ <SelectValue placeholder={t("panel.config.selectFontWeight")} />
2991
3013
  </SelectTrigger>
2992
3014
  <SelectContent>
2993
3015
  <SelectItem value="300">300</SelectItem>
@@ -3001,7 +3023,7 @@ export function PanelConfigSidebar({
3001
3023
  </div>
3002
3024
  <div className="grid grid-cols-2 gap-2">
3003
3025
  <label className="block space-y-1">
3004
- <div>对齐</div>
3026
+ <div>{t("panel.config.textAlign")}</div>
3005
3027
  <Select
3006
3028
  value={selectedElement.textAlign ?? "left"}
3007
3029
  onValueChange={(value) =>
@@ -3011,18 +3033,18 @@ export function PanelConfigSidebar({
3011
3033
  }
3012
3034
  >
3013
3035
  <SelectTrigger className="h-7">
3014
- <SelectValue placeholder="选择对齐方式" />
3036
+ <SelectValue placeholder={t("panel.config.selectTextAlign")} />
3015
3037
  </SelectTrigger>
3016
3038
  <SelectContent>
3017
- <SelectItem value="left">左对齐</SelectItem>
3018
- <SelectItem value="center">居中</SelectItem>
3019
- <SelectItem value="right">右对齐</SelectItem>
3020
- <SelectItem value="justify">两端对齐</SelectItem>
3039
+ <SelectItem value="left">{t("panel.config.alignLeft")}</SelectItem>
3040
+ <SelectItem value="center">{t("panel.config.alignCenter")}</SelectItem>
3041
+ <SelectItem value="right">{t("panel.config.alignRight")}</SelectItem>
3042
+ <SelectItem value="justify">{t("panel.config.alignJustify")}</SelectItem>
3021
3043
  </SelectContent>
3022
3044
  </Select>
3023
3045
  </label>
3024
3046
  <label className="block space-y-1">
3025
- <div>行高</div>
3047
+ <div>{t("panel.config.lineHeight")}</div>
3026
3048
  <Input
3027
3049
  type="number"
3028
3050
  min={1}
@@ -3042,7 +3064,7 @@ export function PanelConfigSidebar({
3042
3064
  </label>
3043
3065
  </div>
3044
3066
  {renderColorField(
3045
- "文字颜色",
3067
+ t("panel.config.textColor"),
3046
3068
  selectedElement.textColor ?? "",
3047
3069
  (next) =>
3048
3070
  updateSelectedText({
@@ -3052,7 +3074,7 @@ export function PanelConfigSidebar({
3052
3074
  </>
3053
3075
  )}
3054
3076
  {renderFieldGroup(
3055
- "输入能力",
3077
+ t("panel.config.groupInputAbility"),
3056
3078
  <label className="flex items-center gap-2">
3057
3079
  <Checkbox
3058
3080
  checked={selectedElement.textAllowInput ?? true}
@@ -3063,23 +3085,23 @@ export function PanelConfigSidebar({
3063
3085
  })
3064
3086
  }
3065
3087
  />
3066
- <span>允许在画布内直接输入(默认开启)</span>
3088
+ <span>{t("panel.config.allowCanvasInput")}</span>
3067
3089
  </label>
3068
3090
  )}
3069
3091
  </>,
3070
3092
  true,
3071
- ["文本", "富文本", "字体", "颜色", "对齐", "行高", "输入"]
3093
+ [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")]
3072
3094
  )
3073
3095
  ) : selectedElement.materialType === "audio" ? (
3074
3096
  renderSection(
3075
3097
  "audioConfig",
3076
- "音频配置",
3098
+ t("panel.config.sectionAudio"),
3077
3099
  <>
3078
3100
  {renderFieldGroup(
3079
- "音频来源",
3101
+ t("panel.config.groupAudioSource"),
3080
3102
  <>
3081
3103
  <label className="block space-y-1">
3082
- <div>音频 URL</div>
3104
+ <div>{t("panel.config.audioUrl")}</div>
3083
3105
  <Input
3084
3106
  value={selectedElement.audioRemoteUrl ?? ""}
3085
3107
  onChange={(e) =>
@@ -3094,7 +3116,7 @@ export function PanelConfigSidebar({
3094
3116
  </label>
3095
3117
  <div className="flex items-center gap-2">
3096
3118
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3097
- 上传音频
3119
+ {t("panel.config.uploadAudio")}
3098
3120
  <Input
3099
3121
  type="file"
3100
3122
  accept="audio/*"
@@ -3112,7 +3134,7 @@ export function PanelConfigSidebar({
3112
3134
  className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent disabled:opacity-50"
3113
3135
  onClick={isRecordingAudio ? stopRecordingAudio : startRecordingAudio}
3114
3136
  >
3115
- {isRecordingAudio ? "停止录音" : "开始录音"}
3137
+ {isRecordingAudio ? t("panel.config.stopRecord") : t("panel.config.startRecord")}
3116
3138
  </button>
3117
3139
  </div>
3118
3140
  {audioStatus ? (
@@ -3128,7 +3150,7 @@ export function PanelConfigSidebar({
3128
3150
  </>
3129
3151
  )}
3130
3152
  {renderFieldGroup(
3131
- "展示样式",
3153
+ t("panel.config.groupDisplayStyle"),
3132
3154
  <>
3133
3155
  <label className="flex items-center gap-2">
3134
3156
  <Checkbox
@@ -3140,10 +3162,10 @@ export function PanelConfigSidebar({
3140
3162
  })
3141
3163
  }
3142
3164
  />
3143
- <span>编辑时自动暂停媒体</span>
3165
+ <span>{t("panel.config.autoPauseMedia")}</span>
3144
3166
  </label>
3145
3167
  <label className="block space-y-1">
3146
- <div>预设喇叭图标</div>
3168
+ <div>{t("panel.config.audioIconPreset")}</div>
3147
3169
  <Select
3148
3170
  value={selectedElement.audioIconPreset ?? "__none__"}
3149
3171
  onValueChange={(value) =>
@@ -3156,19 +3178,19 @@ export function PanelConfigSidebar({
3156
3178
  }
3157
3179
  >
3158
3180
  <SelectTrigger className="h-7">
3159
- <SelectValue placeholder="选择图标" />
3181
+ <SelectValue placeholder={t("panel.config.selectIcon")} />
3160
3182
  </SelectTrigger>
3161
3183
  <SelectContent>
3162
- <SelectItem value="__none__">默认(显示进度条)</SelectItem>
3163
- <SelectItem value="speaker">喇叭</SelectItem>
3164
- <SelectItem value="music">音符</SelectItem>
3165
- <SelectItem value="headphone">耳机</SelectItem>
3166
- <SelectItem value="wave">声波</SelectItem>
3184
+ <SelectItem value="__none__">{t("panel.config.iconDefaultProgress")}</SelectItem>
3185
+ <SelectItem value="speaker">{t("panel.config.iconSpeaker")}</SelectItem>
3186
+ <SelectItem value="music">{t("panel.config.iconMusic")}</SelectItem>
3187
+ <SelectItem value="headphone">{t("panel.config.iconHeadphone")}</SelectItem>
3188
+ <SelectItem value="wave">{t("panel.config.iconWave")}</SelectItem>
3167
3189
  </SelectContent>
3168
3190
  </Select>
3169
3191
  </label>
3170
3192
  <label className="block space-y-1">
3171
- <div>播放动效</div>
3193
+ <div>{t("panel.config.visualEffect")}</div>
3172
3194
  <Select
3173
3195
  value={selectedElement.audioVisualEffect ?? "pulse"}
3174
3196
  onValueChange={(value) =>
@@ -3178,17 +3200,17 @@ export function PanelConfigSidebar({
3178
3200
  }
3179
3201
  >
3180
3202
  <SelectTrigger className="h-7">
3181
- <SelectValue placeholder="选择动效" />
3203
+ <SelectValue placeholder={t("panel.config.selectEffect")} />
3182
3204
  </SelectTrigger>
3183
3205
  <SelectContent>
3184
- <SelectItem value="none">无动效</SelectItem>
3185
- <SelectItem value="pulse">呼吸高亮</SelectItem>
3186
- <SelectItem value="ripple">波纹扩散</SelectItem>
3206
+ <SelectItem value="none">{t("panel.config.effectNone")}</SelectItem>
3207
+ <SelectItem value="pulse">{t("panel.config.effectPulse")}</SelectItem>
3208
+ <SelectItem value="ripple">{t("panel.config.effectRipple")}</SelectItem>
3187
3209
  </SelectContent>
3188
3210
  </Select>
3189
3211
  </label>
3190
3212
  <label className="block space-y-1">
3191
- <div>动效速度</div>
3213
+ <div>{t("panel.config.effectSpeed")}</div>
3192
3214
  <Select
3193
3215
  value={selectedElement.audioVisualSpeed ?? "normal"}
3194
3216
  onValueChange={(value) =>
@@ -3198,18 +3220,18 @@ export function PanelConfigSidebar({
3198
3220
  }
3199
3221
  >
3200
3222
  <SelectTrigger className="h-7">
3201
- <SelectValue placeholder="选择速度" />
3223
+ <SelectValue placeholder={t("panel.config.selectSpeed")} />
3202
3224
  </SelectTrigger>
3203
3225
  <SelectContent>
3204
- <SelectItem value="slow">慢</SelectItem>
3205
- <SelectItem value="normal">中</SelectItem>
3206
- <SelectItem value="fast">快</SelectItem>
3226
+ <SelectItem value="slow">{t("panel.config.speedSlow")}</SelectItem>
3227
+ <SelectItem value="normal">{t("panel.config.speedNormal")}</SelectItem>
3228
+ <SelectItem value="fast">{t("panel.config.speedFast")}</SelectItem>
3207
3229
  </SelectContent>
3208
3230
  </Select>
3209
3231
  </label>
3210
3232
  <div className="flex items-center gap-2">
3211
3233
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3212
- 上传占位图
3234
+ {t("panel.config.uploadPoster")}
3213
3235
  <Input
3214
3236
  type="file"
3215
3237
  accept="image/*"
@@ -3231,35 +3253,38 @@ export function PanelConfigSidebar({
3231
3253
  })
3232
3254
  }
3233
3255
  >
3234
- 清空占位图
3256
+ {t("panel.config.clearPoster")}
3235
3257
  </button>
3236
3258
  </div>
3237
3259
  {selectedElement.audioPosterImage ? (
3238
3260
  <img
3239
3261
  src={selectedElement.audioPosterImage}
3240
- alt="音频占位图预览"
3262
+ alt={t("panel.config.audioPosterAlt")}
3241
3263
  className="h-20 w-full rounded border border-border/60 object-cover"
3242
3264
  />
3243
3265
  ) : null}
3244
- <div className="text-[11px] text-muted-foreground">
3245
- 设置占位图或图标后,节点上将隐藏进度条,改为点击图标播放/暂停。
3266
+ <div className="flex items-center gap-1">
3267
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.audioPoster")}</div>
3268
+ <ConfigHintIcon label={t("panel.config.audioPoster")}>
3269
+ {t("panel.config.audioPosterHint")}
3270
+ </ConfigHintIcon>
3246
3271
  </div>
3247
3272
  </>
3248
3273
  )}
3249
3274
  </>,
3250
3275
  true,
3251
- ["音频", "url", "上传", "录音", "icon", "占位", "动效", "自动暂停", "media"]
3276
+ [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
3277
  )
3253
3278
  ) : selectedElement.materialType === "video" ? (
3254
3279
  renderSection(
3255
3280
  "videoConfig",
3256
- "视频配置",
3281
+ t("panel.config.sectionVideo"),
3257
3282
  <>
3258
3283
  {renderFieldGroup(
3259
- "视频来源",
3284
+ t("panel.config.groupVideoSource"),
3260
3285
  <>
3261
3286
  <label className="block space-y-1">
3262
- <div>视频 URL</div>
3287
+ <div>{t("panel.config.videoUrl")}</div>
3263
3288
  <Input
3264
3289
  value={selectedElement.videoRemoteUrl ?? ""}
3265
3290
  onChange={(e) =>
@@ -3274,7 +3299,7 @@ export function PanelConfigSidebar({
3274
3299
  </label>
3275
3300
  <div className="flex items-center gap-2">
3276
3301
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3277
- 上传视频
3302
+ {t("panel.config.uploadVideo")}
3278
3303
  <Input
3279
3304
  type="file"
3280
3305
  accept="video/*"
@@ -3308,26 +3333,26 @@ export function PanelConfigSidebar({
3308
3333
  })
3309
3334
  }
3310
3335
  />
3311
- <span>编辑时自动暂停媒体</span>
3336
+ <span>{t("panel.config.autoPauseMedia")}</span>
3312
3337
  </label>
3313
3338
  </>
3314
3339
  )}
3315
3340
  </>,
3316
3341
  true,
3317
- ["视频", "url", "上传", "预览", "自动暂停", "media"]
3342
+ [t("panel.material.video"), "url", t("panel.config.searchKwUpload"), t("panel.config.searchKwPreview"), t("panel.config.searchKwAutoPause"), "media"]
3318
3343
  )
3319
3344
  ) : null}
3320
3345
  {selectedElement
3321
3346
  ? selectedElement.parentGridId
3322
3347
  ? renderSection(
3323
3348
  "gridChildSpan",
3324
- "网格子节点占位",
3349
+ t("panel.config.sectionGridChildSpan"),
3325
3350
  <>
3326
3351
  {renderFieldGroup(
3327
- "跨槽位",
3352
+ t("panel.config.groupCrossSlots"),
3328
3353
  <div className="grid grid-cols-2 gap-2">
3329
3354
  <label className="block space-y-1">
3330
- <div>跨列(colSpan)</div>
3355
+ <div>{t("panel.config.colSpan")}</div>
3331
3356
  <Input
3332
3357
  type="number"
3333
3358
  min={1}
@@ -3342,7 +3367,7 @@ export function PanelConfigSidebar({
3342
3367
  />
3343
3368
  </label>
3344
3369
  <label className="block space-y-1">
3345
- <div>跨行(rowSpan)</div>
3370
+ <div>{t("panel.config.rowSpan")}</div>
3346
3371
  <Input
3347
3372
  type="number"
3348
3373
  min={1}
@@ -3358,25 +3383,23 @@ export function PanelConfigSidebar({
3358
3383
  </label>
3359
3384
  </div>
3360
3385
  )}
3361
- <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
3362
- 网格子节点可跨越多格,占据更大区域,便于复杂布局。
3363
- </div>
3364
3386
  </>,
3365
3387
  true,
3366
- ["网格", "跨列", "跨行", "span", "slot"]
3388
+ [t("panel.config.searchKwGrid"), t("panel.config.searchKwCrossCol"), t("panel.config.searchKwCrossRow"), "span", "slot"],
3389
+ <>{t("panel.config.gridChildHint")}</>
3367
3390
  )
3368
3391
  : null
3369
3392
  : null}
3370
3393
  {selectedElement.materialType === "geometry" ? (
3371
3394
  renderSection(
3372
3395
  "geometryConfig",
3373
- "几何配置",
3396
+ t("panel.config.sectionGeometry"),
3374
3397
  <>
3375
3398
  {renderFieldGroup(
3376
- "基础形状",
3399
+ t("panel.config.groupBasicShape"),
3377
3400
  <>
3378
3401
  <label className="block space-y-1">
3379
- <div>形状</div>
3402
+ <div>{t("panel.config.shape")}</div>
3380
3403
  <Select
3381
3404
  value={selectedElement.geometryShape ?? "rect"}
3382
3405
  onValueChange={(value) =>
@@ -3386,47 +3409,47 @@ export function PanelConfigSidebar({
3386
3409
  }
3387
3410
  >
3388
3411
  <SelectTrigger className="h-7">
3389
- <SelectValue placeholder="选择形状" />
3412
+ <SelectValue placeholder={t("panel.config.selectShape")} />
3390
3413
  </SelectTrigger>
3391
3414
  <SelectContent>
3392
- <SelectItem value="rect">矩形</SelectItem>
3393
- <SelectItem value="circle">圆形</SelectItem>
3394
- <SelectItem value="triangle">三角形</SelectItem>
3395
- <SelectItem value="diamond">菱形</SelectItem>
3396
- <SelectItem value="hexagon">六边形</SelectItem>
3397
- <SelectItem value="star">星形</SelectItem>
3398
- <SelectItem value="heart">爱心</SelectItem>
3415
+ <SelectItem value="rect">{t("panel.config.shapeRect")}</SelectItem>
3416
+ <SelectItem value="circle">{t("panel.config.shapeCircle")}</SelectItem>
3417
+ <SelectItem value="triangle">{t("panel.config.shapeTriangle")}</SelectItem>
3418
+ <SelectItem value="diamond">{t("panel.config.shapeDiamond")}</SelectItem>
3419
+ <SelectItem value="hexagon">{t("panel.config.shapeHexagon")}</SelectItem>
3420
+ <SelectItem value="star">{t("panel.config.shapeStar")}</SelectItem>
3421
+ <SelectItem value="heart">{t("panel.config.shapeHeart")}</SelectItem>
3399
3422
  </SelectContent>
3400
3423
  </Select>
3401
3424
  </label>
3402
3425
  {renderColorField(
3403
- "几何颜色",
3426
+ t("panel.config.geometryColor"),
3404
3427
  selectedElement.geometryColor ?? "#3b82f6",
3405
3428
  (next) => updateSelectedGeometry({ geometryColor: next || "#3b82f6" })
3406
3429
  )}
3407
3430
  </>
3408
3431
  )}
3409
3432
  {renderFieldGroup(
3410
- "高级(Canvas 脚本)",
3433
+ t("panel.config.groupCanvasScript"),
3411
3434
  <>
3412
- <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
3413
- 可输入 Canvas 绘制逻辑,变量:ctx、width、height、element。脚本异常会被安全忽略。
3414
- </div>
3415
3435
  <Textarea
3416
3436
  value={selectedElement.geometryScript ?? ""}
3417
3437
  onChange={(e) => updateSelectedGeometry({ geometryScript: e.target.value || undefined })}
3418
3438
  spellCheck={false}
3419
3439
  className="h-36 font-mono text-[11px]"
3420
- placeholder="// 例: ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.fillRect(8,8,width-16,height-16);"
3440
+ placeholder={t("panel.config.geometryScriptPlaceholder")}
3421
3441
  />
3442
+ </>,
3443
+ <>
3444
+ {t("panel.config.canvasScriptHint")}
3422
3445
  </>
3423
3446
  )}
3424
3447
  {renderFieldGroup(
3425
- "手绘叠加",
3448
+ t("panel.config.groupSketchOverlay"),
3426
3449
  <>
3427
3450
  <div className="flex items-center gap-2">
3428
3451
  <label className="block space-y-1">
3429
- <div className="text-[11px]">画笔颜色</div>
3452
+ <div className="text-[11px]">{t("panel.config.penColor")}</div>
3430
3453
  <Input
3431
3454
  type="color"
3432
3455
  value={geometryDrawPenColor}
@@ -3435,7 +3458,7 @@ export function PanelConfigSidebar({
3435
3458
  />
3436
3459
  </label>
3437
3460
  <label className="block space-y-1">
3438
- <div className="text-[11px]">画笔粗细</div>
3461
+ <div className="text-[11px]">{t("panel.config.penWidth")}</div>
3439
3462
  <Input
3440
3463
  type="number"
3441
3464
  min={1}
@@ -3503,7 +3526,7 @@ export function PanelConfigSidebar({
3503
3526
  updateSelectedGeometry({ geometrySketchDataUrl: canvas.toDataURL("image/png") });
3504
3527
  }}
3505
3528
  >
3506
- 应用手绘到节点
3529
+ {t("panel.config.applySketch")}
3507
3530
  </button>
3508
3531
  <button
3509
3532
  type="button"
@@ -3513,25 +3536,25 @@ export function PanelConfigSidebar({
3513
3536
  redrawGeometryPadFromElement();
3514
3537
  }}
3515
3538
  >
3516
- 清空手绘
3539
+ {t("panel.config.clearSketch")}
3517
3540
  </button>
3518
3541
  </div>
3519
3542
  </>
3520
3543
  )}
3521
3544
  </>,
3522
3545
  true,
3523
- ["几何", "geometry", "形状", "canvas", "脚本", "手绘"]
3546
+ [t("panel.material.geometry"), "geometry", t("panel.config.shape"), "canvas", t("panel.config.searchKwScript"), t("panel.config.searchKwSketch")]
3524
3547
  )
3525
3548
  ) : selectedElement.materialType === "grid" ? (
3526
3549
  renderSection(
3527
3550
  "gridConfig",
3528
- "网格布局配置",
3551
+ t("panel.config.sectionGrid"),
3529
3552
  <>
3530
3553
  {renderFieldGroup(
3531
- "网格参数",
3554
+ t("panel.config.groupGridParams"),
3532
3555
  <div className="grid grid-cols-2 gap-2">
3533
3556
  <label className="block space-y-1">
3534
- <div>行数</div>
3557
+ <div>{t("panel.config.rowCount")}</div>
3535
3558
  <Input
3536
3559
  type="number"
3537
3560
  min={1}
@@ -3546,7 +3569,7 @@ export function PanelConfigSidebar({
3546
3569
  />
3547
3570
  </label>
3548
3571
  <label className="block space-y-1">
3549
- <div>列数</div>
3572
+ <div>{t("panel.config.colCount")}</div>
3550
3573
  <Input
3551
3574
  type="number"
3552
3575
  min={1}
@@ -3561,7 +3584,7 @@ export function PanelConfigSidebar({
3561
3584
  />
3562
3585
  </label>
3563
3586
  <label className="block space-y-1">
3564
- <div>间距(px)</div>
3587
+ <div>{t("panel.config.gapPx")}</div>
3565
3588
  <Input
3566
3589
  type="number"
3567
3590
  min={0}
@@ -3576,7 +3599,7 @@ export function PanelConfigSidebar({
3576
3599
  />
3577
3600
  </label>
3578
3601
  <label className="block space-y-1">
3579
- <div>内边距(px)</div>
3602
+ <div>{t("panel.config.paddingPx")}</div>
3580
3603
  <Input
3581
3604
  type="number"
3582
3605
  min={0}
@@ -3591,7 +3614,7 @@ export function PanelConfigSidebar({
3591
3614
  />
3592
3615
  </label>
3593
3616
  <label className="block space-y-1">
3594
- <div>吸附阈值(px)</div>
3617
+ <div>{t("panel.config.snapThresholdPx")}</div>
3595
3618
  <Input
3596
3619
  type="number"
3597
3620
  min={8}
@@ -3610,22 +3633,22 @@ export function PanelConfigSidebar({
3610
3633
  </label>
3611
3634
  </div>
3612
3635
  )}
3613
- <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
3614
- 其他节点拖拽靠近该网格槽位中心时会自动吸附,并在节点树显示为该网格子节点。
3615
- </div>
3616
3636
  </>,
3617
3637
  true,
3618
- ["网格", "grid", "行", "列", "间距", "内边距", "吸附", "阈值"]
3638
+ [t("panel.config.searchKwGrid"), "grid", t("panel.config.rows"), t("panel.config.cols"), t("panel.config.searchKwGap"), t("panel.config.searchKwPadding"), t("panel.config.searchKwSnap"), t("panel.config.searchKwThreshold")],
3639
+ <>
3640
+ {t("panel.config.gridHint")}
3641
+ </>
3619
3642
  )
3620
3643
  ) : selectedElement.materialType === "reference" ? (
3621
3644
  renderSection(
3622
3645
  "reference",
3623
- "引用组件配置",
3646
+ t("panel.config.sectionReference"),
3624
3647
  <>
3625
3648
  {renderFieldGroup(
3626
- "引用源",
3649
+ t("panel.config.groupRefSource"),
3627
3650
  <label className="block space-y-1">
3628
- <div>引用图层</div>
3651
+ <div>{t("panel.config.refLayer")}</div>
3629
3652
  <Select
3630
3653
  value={selectedElement.refLayerId ?? "__none__"}
3631
3654
  onValueChange={(value) =>
@@ -3635,10 +3658,10 @@ export function PanelConfigSidebar({
3635
3658
  }
3636
3659
  >
3637
3660
  <SelectTrigger className="h-7">
3638
- <SelectValue placeholder="请选择图层" />
3661
+ <SelectValue placeholder={t("panel.config.pleaseSelectLayer")} />
3639
3662
  </SelectTrigger>
3640
3663
  <SelectContent>
3641
- <SelectItem value="__none__">无(不引用)</SelectItem>
3664
+ <SelectItem value="__none__">{t("panel.config.noneNoRef")}</SelectItem>
3642
3665
  {layers
3643
3666
  .filter((l) => l.id !== selectedElement.layerId)
3644
3667
  .map((l) => (
@@ -3651,10 +3674,10 @@ export function PanelConfigSidebar({
3651
3674
  </label>
3652
3675
  )}
3653
3676
  {renderFieldGroup(
3654
- "拷贝策略",
3677
+ t("panel.config.groupCopyStrategy"),
3655
3678
  <>
3656
3679
  <label className="block space-y-1">
3657
- <div>拷贝模式</div>
3680
+ <div>{t("panel.config.copyMode")}</div>
3658
3681
  <Select
3659
3682
  value={selectedElement.refCopyMode ?? "shallow"}
3660
3683
  onValueChange={(value) =>
@@ -3665,31 +3688,31 @@ export function PanelConfigSidebar({
3665
3688
  }
3666
3689
  >
3667
3690
  <SelectTrigger className="h-7">
3668
- <SelectValue placeholder="请选择拷贝模式" />
3691
+ <SelectValue placeholder={t("panel.config.selectCopyMode")} />
3669
3692
  </SelectTrigger>
3670
3693
  <SelectContent>
3671
- <SelectItem value="shallow">浅拷贝(跟随源图层变化)</SelectItem>
3672
- <SelectItem value="deep">深拷贝(冻结当前引用快照)</SelectItem>
3694
+ <SelectItem value="shallow">{t("panel.config.shallowFollow")}</SelectItem>
3695
+ <SelectItem value="deep">{t("panel.config.deepFreeze")}</SelectItem>
3673
3696
  </SelectContent>
3674
3697
  </Select>
3675
3698
  </label>
3676
- <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
3677
- 浅拷贝会实时同步被引用图层;深拷贝会固定当前快照,不再随源变化。
3678
- </div>
3699
+ </>,
3700
+ <>
3701
+ {t("panel.config.copyStrategyHint")}
3679
3702
  </>
3680
3703
  )}
3681
3704
  </>,
3682
3705
  true,
3683
- ["引用", "ref", "图层", "浅拷贝", "深拷贝", "snapshot"]
3706
+ [t("panel.config.searchKwRef"), "ref", t("panel.config.layer"), t("panel.material.shallowCopy"), t("panel.material.deepCopy"), "snapshot"]
3684
3707
  )
3685
3708
  ) : (
3686
3709
  <div className="text-xs leading-6 text-muted-foreground">
3687
- 当前节点不是图表类型,暂无图表配置项。
3710
+ {t("panel.config.notChartType")}
3688
3711
  </div>
3689
3712
  )}
3690
3713
  {hasSearch && renderedSectionCount === 0 ? (
3691
3714
  <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
3692
- 未找到匹配项,请尝试更换关键词。
3715
+ {t("panel.config.noMatch")}
3693
3716
  </div>
3694
3717
  ) : null}
3695
3718
  </div>