@arronqzy/react-view 1.0.5 → 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 (36) hide show
  1. package/dist/styles.css +519 -9
  2. package/package.json +12 -4
  3. package/src/core/template.tsx +12 -2
  4. package/src/panel/ReactViewOnlinePreview.tsx +18 -8
  5. package/src/panel/ReactViewPanel.tsx +386 -170
  6. package/src/panel/ai/AssistantChatPanel.tsx +895 -0
  7. package/src/panel/ai/AssistantMarkdown.tsx +256 -0
  8. package/src/panel/ai/applyAssistantAction.ts +652 -0
  9. package/src/panel/ai/revealConfigField.ts +137 -0
  10. package/src/panel/components/ConfigHintIcon.tsx +6 -2
  11. package/src/panel/components/ElementsLayer.tsx +37 -18
  12. package/src/panel/components/MaterialSidebar.tsx +157 -109
  13. package/src/panel/components/MoveableLayer.tsx +7 -6
  14. package/src/panel/components/PanelConfigSidebar.tsx +434 -389
  15. package/src/panel/components/ViewElementScopePanel.tsx +9 -5
  16. package/src/panel/components/WorkspaceConfigSidebar.tsx +5 -2
  17. package/src/panel/components/WorkspaceProjectNav.tsx +25 -14
  18. package/src/panel/components/WorkspaceStageSplit.tsx +3 -1
  19. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +11 -7
  20. package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +6 -18
  21. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +5 -2
  22. package/src/panel/components/table/ConditionEditor.tsx +205 -0
  23. package/src/panel/components/table/JsonCodeEditor.tsx +115 -0
  24. package/src/panel/components/table/PanelConfigTableSection.tsx +2192 -0
  25. package/src/panel/components/table/TableNodeContent.tsx +544 -0
  26. package/src/panel/constants/messages.ts +76 -34
  27. package/src/panel/hooks/usePanelElements.ts +64 -52
  28. package/src/panel/hooks/useWorkspaceProjects.ts +11 -8
  29. package/src/panel/types.ts +4 -0
  30. package/src/panel/utils/panelElementDefaults.ts +69 -39
  31. package/src/panel/utils/scope-field-labels.ts +51 -38
  32. package/src/panel/utils/scope-template-preview.ts +10 -6
  33. package/src/panel/utils/scope-template-spread.ts +44 -21
  34. package/src/panel/utils/scope-template-warnings.ts +28 -16
  35. package/src/panel/utils/scope-template.ts +82 -2
  36. package/src/tailwind.css +17 -9
@@ -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,
@@ -28,13 +29,18 @@ import type {
28
29
  } from "../types";
29
30
  import { buildChartOption, CHART_TYPES, getChartLabelsDisplayText, getChartValuesDisplayText } from "../utils/chartOptionBuilder";
30
31
  import type { PanelLayer } from "../types";
31
- import { PANEL_MESSAGES } from "../constants/messages";
32
+ import { getPanelMessages } from "../constants/messages";
32
33
  import { ViewElementScopePanel } from "./ViewElementScopePanel";
33
34
  import { ConfigHintIcon } from "./ConfigHintIcon";
34
35
  import { hasViewElementScope } from "../scope/view-scope-store";
35
36
  import { collectElementScopeWarnings } from "../utils/scope-template-warnings";
36
37
  import { ScopeConfigProvider } from "./scope-config/ScopeConfigContext";
37
38
  import { ScopeTemplateWarningsPanel } from "./scope-config/ScopeTemplateWarningsPanel";
39
+ import { PanelConfigTableSection } from "./table/PanelConfigTableSection";
40
+ import {
41
+ highlightConfigField,
42
+ subscribeRevealPanelConfig,
43
+ } from "../ai/revealConfigField";
38
44
 
39
45
  type UpdateElement = (
40
46
  id: string,
@@ -56,6 +62,7 @@ export type PanelConfigSidebarProps = {
56
62
  ) => void;
57
63
  /** 蓝图视图绑定节点写入的信号 scope,用于展示与模版解析 */
58
64
  viewElementScope?: unknown;
65
+ blueprintNodeOptions?: { id: string; label: string }[];
59
66
  };
60
67
 
61
68
  export function PanelConfigSidebar({
@@ -68,7 +75,11 @@ export function PanelConfigSidebar({
68
75
  onExcludeSelectedNode,
69
76
  onAdjustNodeZOrder,
70
77
  viewElementScope,
78
+ blueprintNodeOptions = [],
71
79
  }: PanelConfigSidebarProps) {
80
+ const { t } = useI18n();
81
+ const messages = React.useMemo(() => getPanelMessages(t), [t]);
82
+
72
83
  const [isAdvancedOptionMode, setIsAdvancedOptionMode] = useState(false);
73
84
  const [optionJsonText, setOptionJsonText] = useState("{}");
74
85
  const [optionJsonError, setOptionJsonError] = useState<string | null>(null);
@@ -113,9 +124,9 @@ export function PanelConfigSidebar({
113
124
  const readonlyReason = !selectedElement
114
125
  ? ""
115
126
  : selectedElement.locked
116
- ? PANEL_MESSAGES.nodeConfigLocked
127
+ ? messages.nodeConfigLocked
117
128
  : selectedLayer?.locked
118
- ? PANEL_MESSAGES.nodeConfigLayerLocked
129
+ ? messages.nodeConfigLayerLocked
119
130
  : "";
120
131
  const selectedChartType = (selectedElement?.materialType ?? "") as
121
132
  | "bar"
@@ -135,7 +146,7 @@ export function PanelConfigSidebar({
135
146
  hasViewElementScope(selectedElement.id);
136
147
  const scopeWarnings = useMemo(() => {
137
148
  if (!selectedElement || viewElementScope === undefined) return [];
138
- return collectElementScopeWarnings(selectedElement, viewElementScope);
149
+ return collectElementScopeWarnings(selectedElement, viewElementScope, t);
139
150
  }, [selectedElement, viewElementScope]);
140
151
  const effectiveSelectedElements = useMemo(() => {
141
152
  if (selectedElements.length > 0) return selectedElements;
@@ -168,7 +179,8 @@ export function PanelConfigSidebar({
168
179
 
169
180
  useEffect(() => {
170
181
  if (!selectedElement || selectedElement.materialType !== "text") return;
171
- const nextHtml = selectedElement.textHtml ?? "<p>双击输入文本</p>";
182
+ const nextHtml =
183
+ selectedElement.textHtml ?? `<p>${t("panel.defaults.doubleClickTextHtml")}</p>`;
172
184
  if (textEditorRef.current && textEditorRef.current.innerHTML !== nextHtml) {
173
185
  textEditorRef.current.innerHTML = nextHtml;
174
186
  }
@@ -253,13 +265,13 @@ export function PanelConfigSidebar({
253
265
  const base64 = await new Promise<string>((resolve, reject) => {
254
266
  const reader = new FileReader();
255
267
  reader.onload = () => resolve(String(reader.result ?? ""));
256
- reader.onerror = () => reject(new Error(PANEL_MESSAGES.readImageFailed));
268
+ reader.onerror = () => reject(new Error(messages.readImageFailed));
257
269
  reader.readAsDataURL(file);
258
270
  });
259
271
  updateSelectedStyle({
260
272
  backgroundImage: `url("${base64}")`,
261
273
  });
262
- setUploadStatus("已写入 base64");
274
+ setUploadStatus(t("panel.config.uploadWrittenBase64"));
263
275
  try {
264
276
  const form = new FormData();
265
277
  form.append("file", file);
@@ -268,10 +280,10 @@ export function PanelConfigSidebar({
268
280
  const data = (await resp.json()) as { url?: string };
269
281
  if (data.url) {
270
282
  updateSelectedStyle({ backgroundImageRemoteUrl: data.url });
271
- setUploadStatus("已上传服务器并写入 base64");
283
+ setUploadStatus(t("panel.config.uploadServerAndBase64"));
272
284
  }
273
285
  } catch {
274
- setUploadStatus("服务器上传失败,仅保留 base64");
286
+ setUploadStatus(t("panel.config.uploadServerFailedKeepBase64"));
275
287
  }
276
288
  },
277
289
  [selectedElement, updateSelectedStyle]
@@ -289,11 +301,11 @@ export function PanelConfigSidebar({
289
301
  const base64 = await new Promise<string>((resolve, reject) => {
290
302
  const reader = new FileReader();
291
303
  reader.onload = () => resolve(String(reader.result ?? ""));
292
- reader.onerror = () => reject(new Error(PANEL_MESSAGES.readAudioFailed));
304
+ reader.onerror = () => reject(new Error(messages.readAudioFailed));
293
305
  reader.readAsDataURL(file);
294
306
  });
295
307
  updateSelectedAudio({ audioSrc: base64 });
296
- setAudioStatus(PANEL_MESSAGES.audioLocalSaved);
308
+ setAudioStatus(messages.audioLocalSaved);
297
309
  try {
298
310
  const form = new FormData();
299
311
  form.append("file", file);
@@ -302,10 +314,10 @@ export function PanelConfigSidebar({
302
314
  const data = (await resp.json()) as { url?: string };
303
315
  if (data.url) {
304
316
  updateSelectedAudio({ audioRemoteUrl: data.url });
305
- setAudioStatus(PANEL_MESSAGES.audioRemoteUploaded);
317
+ setAudioStatus(messages.audioRemoteUploaded);
306
318
  }
307
319
  } catch {
308
- setAudioStatus(PANEL_MESSAGES.audioServerUploadFailed);
320
+ setAudioStatus(messages.audioServerUploadFailed);
309
321
  }
310
322
  },
311
323
  [selectedElement, updateSelectedAudio]
@@ -316,11 +328,11 @@ export function PanelConfigSidebar({
316
328
  const base64 = await new Promise<string>((resolve, reject) => {
317
329
  const reader = new FileReader();
318
330
  reader.onload = () => resolve(String(reader.result ?? ""));
319
- reader.onerror = () => reject(new Error(PANEL_MESSAGES.readImageFailed));
331
+ reader.onerror = () => reject(new Error(messages.readImageFailed));
320
332
  reader.readAsDataURL(file);
321
333
  });
322
334
  updateSelectedAudio({ audioPosterImage: base64 });
323
- setAudioStatus(PANEL_MESSAGES.audioPosterSet);
335
+ setAudioStatus(messages.audioPosterSet);
324
336
  },
325
337
  [selectedElement, updateSelectedAudio]
326
338
  );
@@ -330,7 +342,7 @@ export function PanelConfigSidebar({
330
342
  const startRecordingAudio = useCallback(async () => {
331
343
  if (!selectedElement || selectedElement.materialType !== "audio") return;
332
344
  if (!navigator.mediaDevices?.getUserMedia) {
333
- setAudioStatus(PANEL_MESSAGES.audioRecordUnsupported);
345
+ setAudioStatus(messages.audioRecordUnsupported);
334
346
  return;
335
347
  }
336
348
  try {
@@ -347,11 +359,11 @@ export function PanelConfigSidebar({
347
359
  const dataUrl = await new Promise<string>((resolve, reject) => {
348
360
  const reader = new FileReader();
349
361
  reader.onload = () => resolve(String(reader.result ?? ""));
350
- reader.onerror = () => reject(new Error(PANEL_MESSAGES.readRecordAudioFailed));
362
+ reader.onerror = () => reject(new Error(messages.readRecordAudioFailed));
351
363
  reader.readAsDataURL(blob);
352
364
  });
353
365
  updateSelectedAudio({ audioSrc: dataUrl });
354
- setAudioStatus(PANEL_MESSAGES.audioRecordSaved);
366
+ setAudioStatus(messages.audioRecordSaved);
355
367
  recordStreamRef.current?.getTracks().forEach((track) => track.stop());
356
368
  recordStreamRef.current = null;
357
369
  recorderRef.current = null;
@@ -359,9 +371,9 @@ export function PanelConfigSidebar({
359
371
  };
360
372
  recorder.start();
361
373
  setIsRecordingAudio(true);
362
- setAudioStatus(PANEL_MESSAGES.audioRecording);
374
+ setAudioStatus(messages.audioRecording);
363
375
  } catch {
364
- setAudioStatus(PANEL_MESSAGES.audioRecordStartFailed);
376
+ setAudioStatus(messages.audioRecordStartFailed);
365
377
  setIsRecordingAudio(false);
366
378
  }
367
379
  }, [selectedElement, updateSelectedAudio]);
@@ -378,11 +390,11 @@ export function PanelConfigSidebar({
378
390
  const base64 = await new Promise<string>((resolve, reject) => {
379
391
  const reader = new FileReader();
380
392
  reader.onload = () => resolve(String(reader.result ?? ""));
381
- reader.onerror = () => reject(new Error(PANEL_MESSAGES.readVideoFailed));
393
+ reader.onerror = () => reject(new Error(messages.readVideoFailed));
382
394
  reader.readAsDataURL(file);
383
395
  });
384
396
  updateSelectedVideo({ videoSrc: base64 });
385
- setVideoStatus(PANEL_MESSAGES.videoLocalSaved);
397
+ setVideoStatus(messages.videoLocalSaved);
386
398
  try {
387
399
  const form = new FormData();
388
400
  form.append("file", file);
@@ -391,10 +403,10 @@ export function PanelConfigSidebar({
391
403
  const data = (await resp.json()) as { url?: string };
392
404
  if (data.url) {
393
405
  updateSelectedVideo({ videoRemoteUrl: data.url });
394
- setVideoStatus(PANEL_MESSAGES.videoRemoteUploaded);
406
+ setVideoStatus(messages.videoRemoteUploaded);
395
407
  }
396
408
  } catch {
397
- setVideoStatus(PANEL_MESSAGES.videoServerUploadFailed);
409
+ setVideoStatus(messages.videoServerUploadFailed);
398
410
  }
399
411
  },
400
412
  [selectedElement, updateSelectedVideo]
@@ -407,6 +419,18 @@ export function PanelConfigSidebar({
407
419
  setExpandedSections((prev) => ({ ...prev, [key]: next }));
408
420
  };
409
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
+
410
434
  const renderSection = (
411
435
  key: string,
412
436
  title: string,
@@ -425,6 +449,7 @@ export function PanelConfigSidebar({
425
449
  open={hasSearch ? true : isSectionExpanded(key, defaultOpen)}
426
450
  onOpenChange={(open) => setSectionExpanded(key, open)}
427
451
  className="rounded-xl border border-border/70 bg-card/95 shadow-sm"
452
+ data-config-section={key}
428
453
  >
429
454
  <div className="flex items-center gap-1.5 px-3 py-2">
430
455
  <CollapsibleTrigger asChild>
@@ -487,7 +512,7 @@ export function PanelConfigSidebar({
487
512
  value={/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value || "") ? value : "#000000"}
488
513
  onChange={(e) => onTextChange(e.target.value)}
489
514
  className="h-7 w-10 cursor-pointer p-1"
490
- aria-label={`${label}调色盘`}
515
+ aria-label={t("common.colorPickerAria", { label })}
491
516
  />
492
517
  </div>
493
518
  </label>
@@ -508,17 +533,17 @@ export function PanelConfigSidebar({
508
533
  </ConfigHintIcon>
509
534
  </div>
510
535
  );
511
- const renderFormatterLabel = (label = "Tooltip Formatter(可选)") => (
536
+ const renderFormatterLabel = (label = t("panel.config.tooltipFormatter")) => (
512
537
  <div className="flex min-w-0 items-center gap-1">
513
538
  <span className={optionLabelTextClass} title={label}>{label}</span>
514
539
  <ConfigHintIcon label="Tooltip Formatter" contentClassName="max-w-[360px]">
515
- <div className="font-medium">可用占位符</div>
516
- <div>{"{a}=系列名, {b}=类目名, {c}=数值, {d}=百分比(饼图)"}</div>
517
- <div className="mt-1 font-medium">常用模板 + 输出示例</div>
518
- <div>{"1) {b}: {c} -> 周一: 120"}</div>
519
- <div>{"2) {a}<br/>{b}: {c} -> 销量<br/>周一: 120"}</div>
520
- <div>{"3) {b}: {c} ({d}%) -> 访问来源: 335 (42%)"}</div>
521
- <div>{"4) ¥{c} / {b} -> ¥120 / 周一"}</div>
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>
522
547
  </ConfigHintIcon>
523
548
  </div>
524
549
  );
@@ -581,11 +606,11 @@ export function PanelConfigSidebar({
581
606
  >
582
607
  <aside
583
608
  ref={sidebarScrollRef}
584
- 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}`}
585
610
  >
586
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">
587
612
  <div className="flex items-center justify-between gap-2">
588
- <div className="text-xs font-semibold tracking-wide">配置面板</div>
613
+ <div className="text-xs font-semibold tracking-wide">{t("panel.config.panelTitle")}</div>
589
614
  </div>
590
615
  {showScopePanel ? (
591
616
  <ViewElementScopePanel scope={viewElementScope} />
@@ -604,7 +629,7 @@ export function PanelConfigSidebar({
604
629
  className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
605
630
  onClick={() => setIsSearchCollapsed((prev) => !prev)}
606
631
  >
607
- {isSearchCollapsed ? "展开搜索" : "收起搜索"}
632
+ {isSearchCollapsed ? t("panel.config.expandSearch") : t("panel.config.collapseSearch")}
608
633
  </button>
609
634
  </div>
610
635
  {!isSearchCollapsed ? (
@@ -612,13 +637,13 @@ export function PanelConfigSidebar({
612
637
  <Input
613
638
  value={configSearch}
614
639
  onChange={(e) => setConfigSearch(e.target.value)}
615
- placeholder="搜索配置,如:边框、tooltip、音频、网格..."
640
+ placeholder={t("panel.config.searchPlaceholder")}
616
641
  className="h-7"
617
642
  data-scope-autocomplete="off"
618
643
  />
619
644
  {hasSearch ? (
620
645
  <div className="mt-1 text-[11px] text-muted-foreground">
621
- 搜索中:{configSearch}
646
+ {t("panel.config.searching", { query: configSearch })}
622
647
  </div>
623
648
  ) : null}
624
649
  </div>
@@ -629,7 +654,7 @@ export function PanelConfigSidebar({
629
654
  <div className="space-y-3">
630
655
  <Card>
631
656
  <CardHeader className="pb-2">
632
- <CardTitle className="text-xs">批量设置({effectiveSelectedElements.length} 个)</CardTitle>
657
+ <CardTitle className="text-xs">{t("panel.config.batchTitleWithCount", { count: effectiveSelectedElements.length })}</CardTitle>
633
658
  </CardHeader>
634
659
  <CardContent className="space-y-2 text-xs">
635
660
  <div className="grid grid-cols-2 gap-2">
@@ -638,14 +663,14 @@ export function PanelConfigSidebar({
638
663
  className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
639
664
  onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: true }))}
640
665
  >
641
- 全部锁定
666
+ {t("panel.config.lockAll")}
642
667
  </button>
643
668
  <button
644
669
  type="button"
645
670
  className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
646
671
  onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: false }))}
647
672
  >
648
- 全部解锁
673
+ {t("panel.config.unlockAll")}
649
674
  </button>
650
675
  <button
651
676
  type="button"
@@ -656,7 +681,7 @@ export function PanelConfigSidebar({
656
681
  )
657
682
  }
658
683
  >
659
- 全部上移一层
684
+ {t("panel.config.bringAllForward")}
660
685
  </button>
661
686
  <button
662
687
  type="button"
@@ -667,7 +692,7 @@ export function PanelConfigSidebar({
667
692
  )
668
693
  }
669
694
  >
670
- 全部下移一层
695
+ {t("panel.config.sendAllBackward")}
671
696
  </button>
672
697
  <button
673
698
  type="button"
@@ -678,7 +703,7 @@ export function PanelConfigSidebar({
678
703
  )
679
704
  }
680
705
  >
681
- 全部 zIndex 设为 1
706
+ {t("panel.config.setAllZIndex1")}
682
707
  </button>
683
708
  <button
684
709
  type="button"
@@ -694,7 +719,7 @@ export function PanelConfigSidebar({
694
719
  )
695
720
  }
696
721
  >
697
- 全部背景色设为蓝色
722
+ {t("panel.config.setAllBgBlue")}
698
723
  </button>
699
724
  </div>
700
725
  </CardContent>
@@ -717,20 +742,20 @@ export function PanelConfigSidebar({
717
742
  type="button"
718
743
  className="inline-flex h-6 w-6 items-center justify-center rounded border border-border text-[11px] hover:bg-accent"
719
744
  onClick={() => setNodeCardExpanded(el.id, !isNodeCardExpanded(el.id))}
720
- aria-label={isNodeCardExpanded(el.id) ? "收起节点配置" : "展开节点配置"}
745
+ aria-label={isNodeCardExpanded(el.id) ? t("panel.config.collapseNodeConfig") : t("panel.config.expandNodeConfig")}
721
746
  >
722
747
  {isNodeCardExpanded(el.id) ? "▾" : "▸"}
723
748
  </button>
724
749
  <CardTitle className="min-w-0 flex-1 text-xs truncate">
725
- {el.name?.trim() || el.materialType || "节点"} · {el.id}
750
+ {el.name?.trim() || el.materialType || t("common.node")} · {el.id}
726
751
  </CardTitle>
727
752
  <button
728
753
  type="button"
729
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"
730
755
  onClick={() => onExcludeSelectedNode?.(el.id)}
731
- title="将该节点从当前多选中剔除"
756
+ title={t("panel.config.removeFromSelectionTitle")}
732
757
  >
733
- 剔除
758
+ {t("panel.config.removeFromSelection")}
734
759
  </button>
735
760
  </div>
736
761
  </CardHeader>
@@ -741,17 +766,17 @@ export function PanelConfigSidebar({
741
766
  checked={el.locked === true}
742
767
  onCheckedChange={(checked) => updateElement(el.id, { locked: checked === true })}
743
768
  />
744
- <span>锁定节点</span>
769
+ <span>{t("panel.config.lockedNode")}</span>
745
770
  </label>
746
771
  {el.locked ? (
747
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">
748
- 当前节点已锁定,仅可操作锁定开关。
773
+ {t("panel.config.lockedNodeHint")}
749
774
  </div>
750
775
  ) : null}
751
776
  <fieldset disabled={el.locked} className={el.locked ? "opacity-60" : ""}>
752
777
  <div className="space-y-2">
753
778
  <label className="block space-y-1">
754
- <div>名称</div>
779
+ <div>{t("panel.config.name")}</div>
755
780
  <Input
756
781
  className="h-7"
757
782
  value={el.name ?? ""}
@@ -771,13 +796,13 @@ export function PanelConfigSidebar({
771
796
  />
772
797
  </label>
773
798
  <label className="block space-y-1">
774
- <div>图层</div>
799
+ <div>{t("panel.config.layer")}</div>
775
800
  <Select
776
801
  value={el.layerId}
777
802
  onValueChange={(value) => updateElement(el.id, { layerId: value })}
778
803
  >
779
804
  <SelectTrigger className="h-7">
780
- <SelectValue placeholder="选择图层" />
805
+ <SelectValue placeholder={t("panel.config.selectLayer")} />
781
806
  </SelectTrigger>
782
807
  <SelectContent>
783
808
  {layers.map((layer) => (
@@ -809,7 +834,7 @@ export function PanelConfigSidebar({
809
834
  />
810
835
  </label>
811
836
  <label className="block space-y-1">
812
- <div>旋转角度</div>
837
+ <div>{t("panel.config.rotate")}</div>
813
838
  <Input
814
839
  className="h-7"
815
840
  type="number"
@@ -820,7 +845,7 @@ export function PanelConfigSidebar({
820
845
  />
821
846
  </label>
822
847
  <label className="block space-y-1">
823
- <div>宽</div>
848
+ <div>{t("panel.config.width")}</div>
824
849
  <Input
825
850
  className="h-7"
826
851
  type="number"
@@ -832,7 +857,7 @@ export function PanelConfigSidebar({
832
857
  />
833
858
  </label>
834
859
  <label className="block space-y-1">
835
- <div>高</div>
860
+ <div>{t("panel.config.height")}</div>
836
861
  <Input
837
862
  className="h-7"
838
863
  type="number"
@@ -846,7 +871,7 @@ export function PanelConfigSidebar({
846
871
  </div>
847
872
  <div className="grid grid-cols-2 gap-2">
848
873
  <label className="block space-y-1">
849
- <div>背景色</div>
874
+ <div>{t("panel.config.backgroundColor")}</div>
850
875
  <Input
851
876
  className="h-7"
852
877
  value={el.style?.backgroundColor ?? ""}
@@ -862,7 +887,7 @@ export function PanelConfigSidebar({
862
887
  />
863
888
  </label>
864
889
  <label className="block space-y-1">
865
- <div>边框色</div>
890
+ <div>{t("panel.config.borderColorShort")}</div>
866
891
  <Input
867
892
  className="h-7"
868
893
  value={el.style?.borderColor ?? ""}
@@ -881,7 +906,7 @@ export function PanelConfigSidebar({
881
906
  {CHART_TYPES.has(el.materialType ?? "") ? (
882
907
  <div className="grid grid-cols-2 gap-2">
883
908
  <label className="block space-y-1 col-span-2">
884
- <div>图表标题</div>
909
+ <div>{t("panel.config.chartTitle")}</div>
885
910
  <Input
886
911
  className="h-7"
887
912
  value={el.chart?.title ?? ""}
@@ -893,7 +918,7 @@ export function PanelConfigSidebar({
893
918
  />
894
919
  </label>
895
920
  <label className="block space-y-1">
896
- <div>主色</div>
921
+ <div>{t("panel.config.primaryColor")}</div>
897
922
  <Input
898
923
  className="h-7"
899
924
  value={el.chart?.color ?? ""}
@@ -913,12 +938,12 @@ export function PanelConfigSidebar({
913
938
  })
914
939
  }
915
940
  />
916
- <span>主色渐变</span>
941
+ <span>{t("panel.config.primaryGradient")}</span>
917
942
  </label>
918
943
  {el.chart?.colorMode === "gradient" ? (
919
944
  <>
920
945
  <label className="block space-y-1">
921
- <div>渐变起始色</div>
946
+ <div>{t("panel.config.gradientFrom")}</div>
922
947
  <Input
923
948
  className="h-7"
924
949
  value={el.chart?.gradientFrom ?? el.chart?.color ?? "#3b82f6"}
@@ -930,7 +955,7 @@ export function PanelConfigSidebar({
930
955
  />
931
956
  </label>
932
957
  <label className="block space-y-1">
933
- <div>渐变结束色</div>
958
+ <div>{t("panel.config.gradientTo")}</div>
934
959
  <Input
935
960
  className="h-7"
936
961
  value={el.chart?.gradientTo ?? "#22d3ee"}
@@ -942,7 +967,7 @@ export function PanelConfigSidebar({
942
967
  />
943
968
  </label>
944
969
  <label className="block space-y-1 col-span-2">
945
- <div>渐变方向</div>
970
+ <div>{t("panel.config.gradientDirection")}</div>
946
971
  <Select
947
972
  value={el.chart?.gradientDirection ?? "to-right"}
948
973
  onValueChange={(value) =>
@@ -960,15 +985,15 @@ export function PanelConfigSidebar({
960
985
  >
961
986
  <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
962
987
  <SelectContent>
963
- <SelectItem value="to-right">左 → 右</SelectItem>
964
- <SelectItem value="to-bottom">上 → 下</SelectItem>
965
- <SelectItem value="to-bottom-right">左上 → 右下</SelectItem>
966
- <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>
967
992
  </SelectContent>
968
993
  </Select>
969
994
  </label>
970
995
  <div className="col-span-2 space-y-1">
971
- <div className="text-[11px] text-muted-foreground">渐变预览</div>
996
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.gradientPreview")}</div>
972
997
  <div
973
998
  className="h-6 rounded border border-border/60"
974
999
  style={{
@@ -987,7 +1012,7 @@ export function PanelConfigSidebar({
987
1012
  </>
988
1013
  ) : null}
989
1014
  <label className="block space-y-1">
990
- <div>渲染</div>
1015
+ <div>{t("panel.config.render")}</div>
991
1016
  <Select
992
1017
  value={el.chart?.renderMode ?? "canvas"}
993
1018
  onValueChange={(value) =>
@@ -1006,7 +1031,7 @@ export function PanelConfigSidebar({
1006
1031
  </Select>
1007
1032
  </label>
1008
1033
  <label className="block space-y-1">
1009
- <div>Tooltip 背景</div>
1034
+ <div>{t("panel.config.tooltipBg")}</div>
1010
1035
  <Input
1011
1036
  className="h-7"
1012
1037
  value={el.chart?.tooltipBackgroundColor ?? ""}
@@ -1030,10 +1055,10 @@ export function PanelConfigSidebar({
1030
1055
  })
1031
1056
  }
1032
1057
  />
1033
- <span>显示 Tooltip</span>
1058
+ <span>{t("panel.config.showTooltip")}</span>
1034
1059
  </label>
1035
1060
  <label className="block space-y-1">
1036
- <div>Tooltip 触发方式</div>
1061
+ <div>{t("panel.config.tooltipTrigger")}</div>
1037
1062
  <Select
1038
1063
  value={el.chart?.tooltipTrigger ?? "axis"}
1039
1064
  onValueChange={(value) =>
@@ -1050,7 +1075,7 @@ export function PanelConfigSidebar({
1050
1075
  </Select>
1051
1076
  </label>
1052
1077
  <label className="block space-y-1">
1053
- <div>Tooltip 文字色</div>
1078
+ <div>{t("panel.config.tooltipTextColor")}</div>
1054
1079
  <Input
1055
1080
  className="h-7"
1056
1081
  value={el.chart?.tooltipTextColor ?? ""}
@@ -1082,7 +1107,7 @@ export function PanelConfigSidebar({
1082
1107
  />
1083
1108
  </label>
1084
1109
  <label className="block space-y-1 col-span-2">
1085
- <div>类目(逗号分隔)</div>
1110
+ <div>{t("panel.config.labelsCsv")}</div>
1086
1111
  <Input
1087
1112
  className="h-7"
1088
1113
  value={getChartLabelsDisplayText(el.chart)}
@@ -1097,7 +1122,7 @@ export function PanelConfigSidebar({
1097
1122
  />
1098
1123
  </label>
1099
1124
  <label className="block space-y-1 col-span-2">
1100
- <div>数值(逗号分隔)</div>
1125
+ <div>{t("panel.config.valuesCsv")}</div>
1101
1126
  <Input
1102
1127
  className="h-7"
1103
1128
  value={getChartValuesDisplayText(el.chart)}
@@ -1114,7 +1139,7 @@ export function PanelConfigSidebar({
1114
1139
  {["bar", "line", "area", "scatter"].includes(el.materialType ?? "") ? (
1115
1140
  <>
1116
1141
  <label className="block space-y-1">
1117
- <div>X 轴名称</div>
1142
+ <div>{t("panel.config.xAxisName")}</div>
1118
1143
  <Input
1119
1144
  className="h-7"
1120
1145
  value={el.chart?.xAxisName ?? ""}
@@ -1126,7 +1151,7 @@ export function PanelConfigSidebar({
1126
1151
  />
1127
1152
  </label>
1128
1153
  <label className="block space-y-1">
1129
- <div>Y 轴名称</div>
1154
+ <div>{t("panel.config.yAxisName")}</div>
1130
1155
  <Input
1131
1156
  className="h-7"
1132
1157
  value={el.chart?.yAxisName ?? ""}
@@ -1138,7 +1163,7 @@ export function PanelConfigSidebar({
1138
1163
  />
1139
1164
  </label>
1140
1165
  <label className="block space-y-1">
1141
- <div>X 轴标签颜色</div>
1166
+ <div>{t("panel.config.xAxisLabelColor")}</div>
1142
1167
  <Input
1143
1168
  className="h-7"
1144
1169
  value={el.chart?.xAxisLabelColor ?? ""}
@@ -1151,7 +1176,7 @@ export function PanelConfigSidebar({
1151
1176
  />
1152
1177
  </label>
1153
1178
  <label className="block space-y-1">
1154
- <div>Y 轴标签颜色</div>
1179
+ <div>{t("panel.config.yAxisLabelColor")}</div>
1155
1180
  <Input
1156
1181
  className="h-7"
1157
1182
  value={el.chart?.yAxisLabelColor ?? ""}
@@ -1164,7 +1189,7 @@ export function PanelConfigSidebar({
1164
1189
  />
1165
1190
  </label>
1166
1191
  <label className="block space-y-1">
1167
- <div>X 轴标签字号</div>
1192
+ <div>{t("panel.config.xAxisLabelFontSize")}</div>
1168
1193
  <Input
1169
1194
  className="h-7"
1170
1195
  type="number"
@@ -1182,7 +1207,7 @@ export function PanelConfigSidebar({
1182
1207
  />
1183
1208
  </label>
1184
1209
  <label className="block space-y-1">
1185
- <div>Y 轴标签字号</div>
1210
+ <div>{t("panel.config.yAxisLabelFontSize")}</div>
1186
1211
  <Input
1187
1212
  className="h-7"
1188
1213
  type="number"
@@ -1208,7 +1233,7 @@ export function PanelConfigSidebar({
1208
1233
  })
1209
1234
  }
1210
1235
  />
1211
- <span>X 轴标签自动缩略</span>
1236
+ <span>{t("panel.config.xAxisLabelAutoEllipsis")}</span>
1212
1237
  </label>
1213
1238
  <label className="flex items-center gap-2">
1214
1239
  <Checkbox
@@ -1219,7 +1244,7 @@ export function PanelConfigSidebar({
1219
1244
  })
1220
1245
  }
1221
1246
  />
1222
- <span>Y 轴标签自动缩略</span>
1247
+ <span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
1223
1248
  </label>
1224
1249
  <label className="flex items-center gap-2">
1225
1250
  <Checkbox
@@ -1230,7 +1255,7 @@ export function PanelConfigSidebar({
1230
1255
  })
1231
1256
  }
1232
1257
  />
1233
- <span>X 轴刻度线</span>
1258
+ <span>{t("panel.config.xAxisTick")}</span>
1234
1259
  </label>
1235
1260
  <label className="flex items-center gap-2">
1236
1261
  <Checkbox
@@ -1241,10 +1266,10 @@ export function PanelConfigSidebar({
1241
1266
  })
1242
1267
  }
1243
1268
  />
1244
- <span>Y 轴刻度线</span>
1269
+ <span>{t("panel.config.yAxisTick")}</span>
1245
1270
  </label>
1246
1271
  <label className="block space-y-1">
1247
- <div>X 轴刻度线颜色</div>
1272
+ <div>{t("panel.config.xAxisTickColor")}</div>
1248
1273
  <Input
1249
1274
  className="h-7"
1250
1275
  value={el.chart?.xAxisTickColor ?? ""}
@@ -1257,7 +1282,7 @@ export function PanelConfigSidebar({
1257
1282
  />
1258
1283
  </label>
1259
1284
  <label className="block space-y-1">
1260
- <div>Y 轴刻度线颜色</div>
1285
+ <div>{t("panel.config.yAxisTickColor")}</div>
1261
1286
  <Input
1262
1287
  className="h-7"
1263
1288
  value={el.chart?.yAxisTickColor ?? ""}
@@ -1278,7 +1303,7 @@ export function PanelConfigSidebar({
1278
1303
  })
1279
1304
  }
1280
1305
  />
1281
- <span>X 轴分割线</span>
1306
+ <span>{t("panel.config.xAxisSplitLine")}</span>
1282
1307
  </label>
1283
1308
  <label className="flex items-center gap-2">
1284
1309
  <Checkbox
@@ -1289,10 +1314,10 @@ export function PanelConfigSidebar({
1289
1314
  })
1290
1315
  }
1291
1316
  />
1292
- <span>Y 轴分割线</span>
1317
+ <span>{t("panel.config.yAxisSplitLine")}</span>
1293
1318
  </label>
1294
1319
  <label className="block space-y-1">
1295
- <div>X 轴分割线颜色</div>
1320
+ <div>{t("panel.config.xAxisSplitLineColor")}</div>
1296
1321
  <Input
1297
1322
  className="h-7"
1298
1323
  value={el.chart?.xAxisSplitLineColor ?? ""}
@@ -1305,7 +1330,7 @@ export function PanelConfigSidebar({
1305
1330
  />
1306
1331
  </label>
1307
1332
  <label className="block space-y-1">
1308
- <div>Y 轴分割线颜色</div>
1333
+ <div>{t("panel.config.yAxisSplitLineColor")}</div>
1309
1334
  <Input
1310
1335
  className="h-7"
1311
1336
  value={el.chart?.yAxisSplitLineColor ?? ""}
@@ -1321,7 +1346,7 @@ export function PanelConfigSidebar({
1321
1346
  ) : null}
1322
1347
  {el.materialType === "gauge" ? (
1323
1348
  <label className="block space-y-1">
1324
- <div>仪表盘值</div>
1349
+ <div>{t("panel.config.gaugeValue")}</div>
1325
1350
  <Input
1326
1351
  className="h-7"
1327
1352
  type="number"
@@ -1339,7 +1364,7 @@ export function PanelConfigSidebar({
1339
1364
  ) : null}
1340
1365
  {el.materialType === "bar" ? (
1341
1366
  <label className="block space-y-1">
1342
- <div>柱宽</div>
1367
+ <div>{t("panel.config.barWidth")}</div>
1343
1368
  <Input
1344
1369
  className="h-7"
1345
1370
  type="number"
@@ -1366,13 +1391,13 @@ export function PanelConfigSidebar({
1366
1391
  })
1367
1392
  }
1368
1393
  />
1369
- <span>平滑曲线</span>
1394
+ <span>{t("panel.config.smooth")}</span>
1370
1395
  </label>
1371
1396
  ) : null}
1372
1397
  {el.materialType === "pie" ? (
1373
1398
  <>
1374
1399
  <label className="block space-y-1">
1375
- <div>内半径</div>
1400
+ <div>{t("panel.config.pieInnerRadius")}</div>
1376
1401
  <Input
1377
1402
  className="h-7"
1378
1403
  type="number"
@@ -1390,7 +1415,7 @@ export function PanelConfigSidebar({
1390
1415
  />
1391
1416
  </label>
1392
1417
  <label className="block space-y-1">
1393
- <div>外半径</div>
1418
+ <div>{t("panel.config.pieOuterRadius")}</div>
1394
1419
  <Input
1395
1420
  className="h-7"
1396
1421
  type="number"
@@ -1410,7 +1435,7 @@ export function PanelConfigSidebar({
1410
1435
  </>
1411
1436
  ) : null}
1412
1437
  <label className="block space-y-1">
1413
- {renderOptionLabel("网格左边距", "grid.left", "控制绘图区左侧留白。")}
1438
+ {renderOptionLabel(t("panel.config.gridLeft"), "grid.left", t("panel.config.gridLeftHint"))}
1414
1439
  <Input
1415
1440
  className="h-7"
1416
1441
  type="number"
@@ -1428,7 +1453,7 @@ export function PanelConfigSidebar({
1428
1453
  />
1429
1454
  </label>
1430
1455
  <label className="block space-y-1">
1431
- {renderOptionLabel("网格右边距", "grid.right", "控制绘图区右侧留白。")}
1456
+ {renderOptionLabel(t("panel.config.gridRight"), "grid.right", t("panel.config.gridRightHint"))}
1432
1457
  <Input
1433
1458
  className="h-7"
1434
1459
  type="number"
@@ -1446,7 +1471,7 @@ export function PanelConfigSidebar({
1446
1471
  />
1447
1472
  </label>
1448
1473
  <label className="block space-y-1">
1449
- {renderOptionLabel("网格上边距", "grid.top", "控制绘图区顶部留白。")}
1474
+ {renderOptionLabel(t("panel.config.gridTop"), "grid.top", t("panel.config.gridTopHint"))}
1450
1475
  <Input
1451
1476
  className="h-7"
1452
1477
  type="number"
@@ -1464,7 +1489,7 @@ export function PanelConfigSidebar({
1464
1489
  />
1465
1490
  </label>
1466
1491
  <label className="block space-y-1">
1467
- {renderOptionLabel("网格下边距", "grid.bottom", "控制绘图区底部留白。")}
1492
+ {renderOptionLabel(t("panel.config.gridBottom"), "grid.bottom", t("panel.config.gridBottomHint"))}
1468
1493
  <Input
1469
1494
  className="h-7"
1470
1495
  type="number"
@@ -1482,7 +1507,7 @@ export function PanelConfigSidebar({
1482
1507
  />
1483
1508
  </label>
1484
1509
  <label className="block space-y-1">
1485
- {renderOptionLabel("图例位置", "legend.top", "设置图例在容器中的停靠位置。")}
1510
+ {renderOptionLabel(t("panel.config.legendPosition"), "legend.top", t("panel.config.legendPositionHint"))}
1486
1511
  <Select
1487
1512
  value={String((el.chart?.option as any)?.legend?.top ?? "top")}
1488
1513
  onValueChange={(value) =>
@@ -1506,7 +1531,7 @@ export function PanelConfigSidebar({
1506
1531
  </Select>
1507
1532
  </label>
1508
1533
  <label className="block space-y-1">
1509
- {renderOptionLabel("图例排列", "legend.orient", "图例横向或纵向排列。")}
1534
+ {renderOptionLabel(t("panel.config.legendOrient"), "legend.orient", t("panel.config.legendOrientHint"))}
1510
1535
  <Select
1511
1536
  value={String((el.chart?.option as any)?.legend?.orient ?? "horizontal")}
1512
1537
  onValueChange={(value) =>
@@ -1541,7 +1566,7 @@ export function PanelConfigSidebar({
1541
1566
  })
1542
1567
  }
1543
1568
  />
1544
- <span>显示图例(legend.show)</span>
1569
+ <span>{t("panel.config.showLegendWithKey")}</span>
1545
1570
  </label>
1546
1571
  <label className="flex items-center gap-2">
1547
1572
  <Checkbox
@@ -1564,7 +1589,7 @@ export function PanelConfigSidebar({
1564
1589
  });
1565
1590
  }}
1566
1591
  />
1567
- <span>内置缩放(dataZoom[type=inside])</span>
1592
+ <span>{t("panel.config.zoomInsideWithKey")}</span>
1568
1593
  </label>
1569
1594
  <label className="flex items-center gap-2">
1570
1595
  <Checkbox
@@ -1587,7 +1612,7 @@ export function PanelConfigSidebar({
1587
1612
  });
1588
1613
  }}
1589
1614
  />
1590
- <span>滑块缩放(dataZoom[type=slider])</span>
1615
+ <span>{t("panel.config.zoomSliderWithKey")}</span>
1591
1616
  </label>
1592
1617
  <label className="flex items-center gap-2">
1593
1618
  <Checkbox
@@ -1603,10 +1628,10 @@ export function PanelConfigSidebar({
1603
1628
  })
1604
1629
  }
1605
1630
  />
1606
- <span>显示轴指示器(axisPointer.show)</span>
1631
+ <span>{t("panel.config.showAxisPointerWithKey")}</span>
1607
1632
  </label>
1608
1633
  <label className="block space-y-1">
1609
- <div>轴指示器类型(axisPointer.type)</div>
1634
+ <div>{t("panel.config.axisPointerTypeWithKey")}</div>
1610
1635
  <Select
1611
1636
  value={String((el.chart?.option as any)?.axisPointer?.type ?? "line")}
1612
1637
  onValueChange={(value) =>
@@ -1629,11 +1654,11 @@ export function PanelConfigSidebar({
1629
1654
  </Select>
1630
1655
  </label>
1631
1656
  <label className="block space-y-1 col-span-2">
1632
- <div>高级 option JSON(覆盖基础配置)</div>
1657
+ <div>{t("panel.config.advancedOptionJson")}</div>
1633
1658
  <Textarea
1634
1659
  className="h-28 font-mono text-[11px]"
1635
1660
  defaultValue={JSON.stringify(el.chart?.option ?? {}, null, 2)}
1636
- placeholder='例如:{"grid":{"left":24}}'
1661
+ placeholder={t("panel.config.advancedJsonPlaceholder")}
1637
1662
  onBlur={(e) => {
1638
1663
  const nextText = e.target.value.trim();
1639
1664
  if (!nextText) {
@@ -1654,7 +1679,7 @@ export function PanelConfigSidebar({
1654
1679
  {el.materialType === "text" ? (
1655
1680
  <div className="grid grid-cols-2 gap-2">
1656
1681
  <label className="block space-y-1 col-span-2">
1657
- <div>文本内容(HTML)</div>
1682
+ <div>{t("panel.config.textHtml")}</div>
1658
1683
  <Textarea
1659
1684
  className="h-24"
1660
1685
  value={el.textHtml ?? ""}
@@ -1662,7 +1687,7 @@ export function PanelConfigSidebar({
1662
1687
  />
1663
1688
  </label>
1664
1689
  <label className="block space-y-1">
1665
- <div>字体大小</div>
1690
+ <div>{t("panel.config.fontSize")}</div>
1666
1691
  <Input
1667
1692
  className="h-7"
1668
1693
  type="number"
@@ -1672,7 +1697,7 @@ export function PanelConfigSidebar({
1672
1697
  />
1673
1698
  </label>
1674
1699
  <label className="block space-y-1">
1675
- <div>文字颜色</div>
1700
+ <div>{t("panel.config.textColor")}</div>
1676
1701
  <Input
1677
1702
  className="h-7"
1678
1703
  value={el.textColor ?? ""}
@@ -1684,7 +1709,7 @@ export function PanelConfigSidebar({
1684
1709
  {el.materialType === "audio" ? (
1685
1710
  <div className="grid grid-cols-2 gap-2">
1686
1711
  <label className="block space-y-1 col-span-2">
1687
- <div>音频 URL</div>
1712
+ <div>{t("panel.config.audioUrl")}</div>
1688
1713
  <Input
1689
1714
  className="h-7"
1690
1715
  value={el.audioRemoteUrl ?? ""}
@@ -1692,7 +1717,7 @@ export function PanelConfigSidebar({
1692
1717
  />
1693
1718
  </label>
1694
1719
  <label className="block space-y-1">
1695
- <div>动效</div>
1720
+ <div>{t("panel.config.effect")}</div>
1696
1721
  <Select
1697
1722
  value={el.audioVisualEffect ?? "pulse"}
1698
1723
  onValueChange={(value) =>
@@ -1708,7 +1733,7 @@ export function PanelConfigSidebar({
1708
1733
  </Select>
1709
1734
  </label>
1710
1735
  <label className="block space-y-1">
1711
- <div>速度</div>
1736
+ <div>{t("panel.config.speed")}</div>
1712
1737
  <Select
1713
1738
  value={el.audioVisualSpeed ?? "normal"}
1714
1739
  onValueChange={(value) =>
@@ -1727,7 +1752,7 @@ export function PanelConfigSidebar({
1727
1752
  ) : null}
1728
1753
  {el.materialType === "video" ? (
1729
1754
  <label className="block space-y-1">
1730
- <div>视频 URL</div>
1755
+ <div>{t("panel.config.videoUrl")}</div>
1731
1756
  <Input
1732
1757
  className="h-7"
1733
1758
  value={el.videoRemoteUrl ?? ""}
@@ -1738,11 +1763,11 @@ export function PanelConfigSidebar({
1738
1763
  {el.materialType === "grid" ? (
1739
1764
  <div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
1740
1765
  <label className="block space-y-1.5">
1741
- <div>行</div>
1766
+ <div>{t("panel.config.rows")}</div>
1742
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) })} />
1743
1768
  </label>
1744
1769
  <label className="block space-y-1.5">
1745
- <div>列</div>
1770
+ <div>{t("panel.config.cols")}</div>
1746
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) })} />
1747
1772
  </label>
1748
1773
  </div>
@@ -1750,7 +1775,7 @@ export function PanelConfigSidebar({
1750
1775
  {el.materialType === "geometry" ? (
1751
1776
  <div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
1752
1777
  <label className="block space-y-1.5">
1753
- <div>形状</div>
1778
+ <div>{t("panel.config.shape")}</div>
1754
1779
  <Select
1755
1780
  value={el.geometryShape ?? "rect"}
1756
1781
  onValueChange={(value) =>
@@ -1759,18 +1784,18 @@ export function PanelConfigSidebar({
1759
1784
  >
1760
1785
  <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1761
1786
  <SelectContent>
1762
- <SelectItem value="rect">矩形</SelectItem>
1763
- <SelectItem value="circle">圆形</SelectItem>
1764
- <SelectItem value="triangle">三角形</SelectItem>
1765
- <SelectItem value="diamond">菱形</SelectItem>
1766
- <SelectItem value="hexagon">六边形</SelectItem>
1767
- <SelectItem value="star">星形</SelectItem>
1768
- <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>
1769
1794
  </SelectContent>
1770
1795
  </Select>
1771
1796
  </label>
1772
1797
  <label className="block space-y-1.5">
1773
- <div>颜色</div>
1798
+ <div>{t("panel.config.color")}</div>
1774
1799
  <Input
1775
1800
  className="h-7"
1776
1801
  value={el.geometryColor ?? "#3b82f6"}
@@ -1782,14 +1807,14 @@ export function PanelConfigSidebar({
1782
1807
  {el.materialType === "reference" ? (
1783
1808
  <div className="grid grid-cols-2 gap-2">
1784
1809
  <label className="block space-y-1.5">
1785
- <div>引用图层</div>
1810
+ <div>{t("panel.config.refLayer")}</div>
1786
1811
  <Select
1787
1812
  value={el.refLayerId ?? "__none__"}
1788
1813
  onValueChange={(value) => updateElement(el.id, { refLayerId: value === "__none__" ? undefined : value })}
1789
1814
  >
1790
1815
  <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1791
1816
  <SelectContent>
1792
- <SelectItem value="__none__">无</SelectItem>
1817
+ <SelectItem value="__none__">{t("common.none")}</SelectItem>
1793
1818
  {layers.filter((l) => l.id !== el.layerId).map((l) => (
1794
1819
  <SelectItem key={l.id} value={l.id}>{l.name}</SelectItem>
1795
1820
  ))}
@@ -1797,7 +1822,7 @@ export function PanelConfigSidebar({
1797
1822
  </Select>
1798
1823
  </label>
1799
1824
  <label className="block space-y-1.5">
1800
- <div>拷贝</div>
1825
+ <div>{t("panel.config.copy")}</div>
1801
1826
  <Select
1802
1827
  value={el.refCopyMode ?? "shallow"}
1803
1828
  onValueChange={(value) => setReferenceCopyMode?.(el.id, value as ReferenceCopyMode)}
@@ -1824,7 +1849,7 @@ export function PanelConfigSidebar({
1824
1849
  return !text.includes(normalizedSearch);
1825
1850
  }) ? (
1826
1851
  <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
1827
- 未匹配到可编辑节点,请更换关键词。
1852
+ {t("panel.config.noEditableMatch")}
1828
1853
  </div>
1829
1854
  ) : null}
1830
1855
  </div>
@@ -1844,8 +1869,8 @@ export function PanelConfigSidebar({
1844
1869
  <path d="M8 9h8M8 12h8M8 15h5" />
1845
1870
  </svg>
1846
1871
  </EmptyIcon>
1847
- <EmptyTitle>暂无可配置节点</EmptyTitle>
1848
- <EmptyDescription>请先在画布中选中一个节点,再到这里进行配置。</EmptyDescription>
1872
+ <EmptyTitle>{t("panel.config.emptyNoNodeTitle")}</EmptyTitle>
1873
+ <EmptyDescription>{t("panel.config.emptyNoNodeDesc")}</EmptyDescription>
1849
1874
  </Empty>
1850
1875
  ) : !isMultiSelectMode && selectedElement ? (
1851
1876
  <div className="space-y-3">
@@ -1866,17 +1891,17 @@ export function PanelConfigSidebar({
1866
1891
  })
1867
1892
  }
1868
1893
  />
1869
- <span>锁定节点(禁止层级/位置/大小/旋转)</span>
1894
+ <span>{t("panel.config.lockNode")}</span>
1870
1895
  </label>
1871
1896
  </div>
1872
1897
  <fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
1873
1898
  <div className="space-y-3.5 text-xs">
1874
1899
  {renderSection(
1875
1900
  "nodeInfo",
1876
- "节点信息",
1901
+ t("panel.config.sectionNodeInfo"),
1877
1902
  <>
1878
- <label className="block space-y-1">
1879
- <div>节点名称</div>
1903
+ <label className="block space-y-1" data-config-field="name">
1904
+ <div>{t("panel.config.nodeName")}</div>
1880
1905
  <Input
1881
1906
  value={selectedElement.name ?? ""}
1882
1907
  onChange={(e) =>
@@ -1884,12 +1909,12 @@ export function PanelConfigSidebar({
1884
1909
  name: e.target.value || undefined,
1885
1910
  })
1886
1911
  }
1887
- placeholder="自定义节点名称(显示在节点树)"
1912
+ placeholder={t("panel.config.nodeNamePlaceholder")}
1888
1913
  className="h-7"
1889
1914
  />
1890
1915
  </label>
1891
1916
  <div className="grid grid-cols-3 gap-2">
1892
- <label className="block space-y-1">
1917
+ <label className="block space-y-1" data-config-field="x">
1893
1918
  <div>X</div>
1894
1919
  <Input
1895
1920
  type="number"
@@ -1902,7 +1927,7 @@ export function PanelConfigSidebar({
1902
1927
  className="h-7"
1903
1928
  />
1904
1929
  </label>
1905
- <label className="block space-y-1">
1930
+ <label className="block space-y-1" data-config-field="y">
1906
1931
  <div>Y</div>
1907
1932
  <Input
1908
1933
  type="number"
@@ -1917,8 +1942,8 @@ export function PanelConfigSidebar({
1917
1942
  </label>
1918
1943
  </div>
1919
1944
  <div className="grid grid-cols-3 gap-2">
1920
- <label className="block space-y-1">
1921
- <div>旋转角度</div>
1945
+ <label className="block space-y-1" data-config-field="rotate">
1946
+ <div>{t("panel.config.rotate")}</div>
1922
1947
  <Input
1923
1948
  type="number"
1924
1949
  value={selectedElement.rotate ?? 0}
@@ -1930,8 +1955,8 @@ export function PanelConfigSidebar({
1930
1955
  className="h-7"
1931
1956
  />
1932
1957
  </label>
1933
- <label className="block space-y-1">
1934
- <div>宽</div>
1958
+ <label className="block space-y-1" data-config-field="width">
1959
+ <div>{t("panel.config.width")}</div>
1935
1960
  <Input
1936
1961
  type="number"
1937
1962
  min={1}
@@ -1945,8 +1970,8 @@ export function PanelConfigSidebar({
1945
1970
  className="h-7"
1946
1971
  />
1947
1972
  </label>
1948
- <label className="block space-y-1">
1949
- <div>高</div>
1973
+ <label className="block space-y-1" data-config-field="height">
1974
+ <div>{t("panel.config.height")}</div>
1950
1975
  <Input
1951
1976
  type="number"
1952
1977
  min={1}
@@ -1961,9 +1986,9 @@ export function PanelConfigSidebar({
1961
1986
  </label>
1962
1987
  </div>
1963
1988
  <div className="space-y-1.5">
1964
- <div className="text-[11px] text-muted-foreground">节点层级</div>
1989
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.nodeZOrder")}</div>
1965
1990
  <div className="text-[11px] text-muted-foreground/90">
1966
- 当前 zIndex:{nodeZOrderLabel ?? "-"}
1991
+ {t("panel.config.currentZIndex", { value: nodeZOrderLabel ?? "-" })}
1967
1992
  </div>
1968
1993
  <div className="grid grid-cols-2 gap-2">
1969
1994
  <button
@@ -1971,52 +1996,52 @@ export function PanelConfigSidebar({
1971
1996
  className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
1972
1997
  onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "bringForward")}
1973
1998
  >
1974
- 上移一层
1999
+ {t("panel.menubar.bringForward")}
1975
2000
  </button>
1976
2001
  <button
1977
2002
  type="button"
1978
2003
  className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
1979
2004
  onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "sendBackward")}
1980
2005
  >
1981
- 下移一层
2006
+ {t("panel.menubar.sendBackward")}
1982
2007
  </button>
1983
2008
  <button
1984
2009
  type="button"
1985
2010
  className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
1986
2011
  onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "bringToFront")}
1987
2012
  >
1988
- 置顶
2013
+ {t("panel.menubar.bringToFront")}
1989
2014
  </button>
1990
2015
  <button
1991
2016
  type="button"
1992
2017
  className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
1993
2018
  onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "sendToBack")}
1994
2019
  >
1995
- 置底
2020
+ {t("panel.menubar.sendToBack")}
1996
2021
  </button>
1997
2022
  </div>
1998
2023
  </div>
1999
2024
  <div className="truncate text-muted-foreground">ID: {selectedElement.id}</div>
2000
- <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>
2001
2026
  </>,
2002
2027
  true,
2003
- ["名称", "id", "类型", "锁定", "locked", "name"]
2028
+ [t("panel.config.name"), "id", t("panel.config.type"), t("panel.layers.lockShort"), "locked", "name"]
2004
2029
  )}
2005
2030
 
2006
2031
  {renderSection(
2007
2032
  "styleBackground",
2008
- "通用样式 / 背景",
2033
+ t("panel.config.sectionStyleBackground"),
2009
2034
  <>
2010
2035
  {renderFieldGroup(
2011
- "背景填充",
2036
+ t("panel.config.groupBgFill"),
2012
2037
  <>
2013
2038
  {renderColorField(
2014
- "背景色",
2039
+ t("panel.config.backgroundColor"),
2015
2040
  selectedElement.style?.backgroundColor ?? "",
2016
2041
  (next) => updateSelectedStyle({ backgroundColor: next || undefined })
2017
2042
  )}
2018
2043
  <label className="block space-y-1">
2019
- <div>背景图</div>
2044
+ <div>{t("panel.config.backgroundImage")}</div>
2020
2045
  <Input
2021
2046
  value={selectedElement.style?.backgroundImage ?? ""}
2022
2047
  onChange={(e) => updateSelectedStyle({ backgroundImage: e.target.value || undefined })}
@@ -2026,7 +2051,7 @@ export function PanelConfigSidebar({
2026
2051
  </label>
2027
2052
  <div className="flex items-center gap-2">
2028
2053
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
2029
- 上传图片
2054
+ {t("panel.config.uploadImage")}
2030
2055
  <Input
2031
2056
  type="file"
2032
2057
  accept="image/*"
@@ -2046,10 +2071,10 @@ export function PanelConfigSidebar({
2046
2071
  </>
2047
2072
  )}
2048
2073
  {renderFieldGroup(
2049
- "背景布局",
2074
+ t("panel.config.groupBgLayout"),
2050
2075
  <div className="grid grid-cols-2 gap-2">
2051
2076
  <label className="block space-y-1">
2052
- <div>背景尺寸</div>
2077
+ <div>{t("panel.config.backgroundSize")}</div>
2053
2078
  <Select
2054
2079
  value={selectedElement.style?.backgroundSize ?? "__none__"}
2055
2080
  onValueChange={(value) =>
@@ -2059,10 +2084,10 @@ export function PanelConfigSidebar({
2059
2084
  }
2060
2085
  >
2061
2086
  <SelectTrigger className="h-7">
2062
- <SelectValue placeholder="选择背景尺寸" />
2087
+ <SelectValue placeholder={t("panel.config.selectBackgroundSize")} />
2063
2088
  </SelectTrigger>
2064
2089
  <SelectContent>
2065
- <SelectItem value="__none__">默认</SelectItem>
2090
+ <SelectItem value="__none__">{t("common.default")}</SelectItem>
2066
2091
  <SelectItem value="cover">cover</SelectItem>
2067
2092
  <SelectItem value="contain">contain</SelectItem>
2068
2093
  <SelectItem value="100% 100%">100% 100%</SelectItem>
@@ -2071,7 +2096,7 @@ export function PanelConfigSidebar({
2071
2096
  </Select>
2072
2097
  </label>
2073
2098
  <label className="block space-y-1">
2074
- <div>背景位置</div>
2099
+ <div>{t("panel.config.backgroundPosition")}</div>
2075
2100
  <Select
2076
2101
  value={selectedElement.style?.backgroundPosition ?? "__none__"}
2077
2102
  onValueChange={(value) =>
@@ -2081,10 +2106,10 @@ export function PanelConfigSidebar({
2081
2106
  }
2082
2107
  >
2083
2108
  <SelectTrigger className="h-7">
2084
- <SelectValue placeholder="选择背景位置" />
2109
+ <SelectValue placeholder={t("panel.config.selectBackgroundPosition")} />
2085
2110
  </SelectTrigger>
2086
2111
  <SelectContent>
2087
- <SelectItem value="__none__">默认</SelectItem>
2112
+ <SelectItem value="__none__">{t("common.default")}</SelectItem>
2088
2113
  <SelectItem value="center">center</SelectItem>
2089
2114
  <SelectItem value="top">top</SelectItem>
2090
2115
  <SelectItem value="bottom">bottom</SelectItem>
@@ -2101,18 +2126,18 @@ export function PanelConfigSidebar({
2101
2126
  )}
2102
2127
  </>,
2103
2128
  true,
2104
- ["背景色", "背景图", "background", "backgroundSize", "backgroundPosition", "布局"]
2129
+ [t("panel.config.backgroundColor"), t("panel.config.backgroundImage"), "background", "backgroundSize", "backgroundPosition", t("panel.config.searchKwLayout")]
2105
2130
  )}
2106
2131
 
2107
2132
  {renderSection(
2108
2133
  "styleBorder",
2109
- "通用样式 / 边框",
2134
+ t("panel.config.sectionStyleBorder"),
2110
2135
  <>
2111
2136
  {renderFieldGroup(
2112
- "边框几何",
2137
+ t("panel.config.groupBorderGeometry"),
2113
2138
  <div className="grid grid-cols-2 gap-2">
2114
2139
  <label className="block space-y-1">
2115
- <div>边框宽度(px)</div>
2140
+ <div>{t("panel.config.borderWidth")}</div>
2116
2141
  <Input
2117
2142
  type="number"
2118
2143
  min={0}
@@ -2124,7 +2149,7 @@ export function PanelConfigSidebar({
2124
2149
  />
2125
2150
  </label>
2126
2151
  <label className="block space-y-1">
2127
- <div>边框圆角(px)</div>
2152
+ <div>{t("panel.config.borderRadius")}</div>
2128
2153
  <Input
2129
2154
  type="number"
2130
2155
  min={0}
@@ -2138,10 +2163,10 @@ export function PanelConfigSidebar({
2138
2163
  </div>
2139
2164
  )}
2140
2165
  {renderFieldGroup(
2141
- "边框视觉",
2166
+ t("panel.config.groupBorderVisual"),
2142
2167
  <div className="grid grid-cols-2 gap-2">
2143
2168
  <label className="block space-y-1">
2144
- <div>边框样式</div>
2169
+ <div>{t("panel.config.borderStyle")}</div>
2145
2170
  <Select
2146
2171
  value={selectedElement.style?.borderStyle ?? "solid"}
2147
2172
  onValueChange={(value) =>
@@ -2151,7 +2176,7 @@ export function PanelConfigSidebar({
2151
2176
  }
2152
2177
  >
2153
2178
  <SelectTrigger className="h-7">
2154
- <SelectValue placeholder="请选择边框样式" />
2179
+ <SelectValue placeholder={t("panel.config.selectBorderStyle")} />
2155
2180
  </SelectTrigger>
2156
2181
  <SelectContent>
2157
2182
  <SelectItem value="none">none</SelectItem>
@@ -2163,7 +2188,7 @@ export function PanelConfigSidebar({
2163
2188
  </Select>
2164
2189
  </label>
2165
2190
  {renderColorField(
2166
- "边框颜色",
2191
+ t("panel.config.borderColor"),
2167
2192
  selectedElement.style?.borderColor ?? "",
2168
2193
  (next) => updateSelectedStyle({ borderColor: next || undefined })
2169
2194
  )}
@@ -2171,20 +2196,20 @@ export function PanelConfigSidebar({
2171
2196
  )}
2172
2197
  </>,
2173
2198
  true,
2174
- ["边框", "border", "宽度", "圆角", "颜色", "样式"]
2199
+ [t("panel.config.searchKwBorder"), "border", t("panel.config.searchKwWidth"), t("panel.config.searchKwRadius"), t("panel.config.color"), t("panel.config.searchKwStyle")]
2175
2200
  )}
2176
2201
 
2177
2202
  {isChartElement ? (
2178
2203
  <>
2179
2204
  {renderSection(
2180
2205
  "chartBasic",
2181
- "图表配置 / 基础",
2206
+ t("panel.config.sectionChartBasic"),
2182
2207
  <>
2183
2208
  {renderFieldGroup(
2184
- "基础显示",
2209
+ t("panel.config.groupBasicDisplay"),
2185
2210
  <>
2186
- <label className="block space-y-1.5">
2187
- <div>标题</div>
2211
+ <label className="block space-y-1.5" data-config-field="chart.title">
2212
+ <div>{t("panel.config.title")}</div>
2188
2213
  <Input
2189
2214
  value={selectedElement.chart?.title ?? ""}
2190
2215
  onChange={(e) => updateSelectedChart({ title: e.target.value })}
@@ -2193,7 +2218,7 @@ export function PanelConfigSidebar({
2193
2218
  </label>
2194
2219
 
2195
2220
  {renderColorField(
2196
- "主色",
2221
+ t("panel.config.primaryColor"),
2197
2222
  selectedElement.chart?.color ?? "#3b82f6",
2198
2223
  (next) => updateSelectedChart({ color: next || "#3b82f6" })
2199
2224
  )}
@@ -2204,22 +2229,22 @@ export function PanelConfigSidebar({
2204
2229
  updateSelectedChart({ colorMode: checked ? "gradient" : "solid" })
2205
2230
  }
2206
2231
  />
2207
- <span>主色使用渐变</span>
2232
+ <span>{t("panel.config.usePrimaryGradient")}</span>
2208
2233
  </label>
2209
2234
  {selectedElement.chart?.colorMode === "gradient" ? (
2210
2235
  <div className="grid grid-cols-2 gap-2">
2211
2236
  {renderColorField(
2212
- "渐变起始色",
2237
+ t("panel.config.gradientFrom"),
2213
2238
  selectedElement.chart?.gradientFrom ?? selectedElement.chart?.color ?? "#3b82f6",
2214
2239
  (next) => updateSelectedChart({ gradientFrom: next || "#3b82f6" })
2215
2240
  )}
2216
2241
  {renderColorField(
2217
- "渐变结束色",
2242
+ t("panel.config.gradientTo"),
2218
2243
  selectedElement.chart?.gradientTo ?? "#22d3ee",
2219
2244
  (next) => updateSelectedChart({ gradientTo: next || "#22d3ee" })
2220
2245
  )}
2221
2246
  <label className="block space-y-1 col-span-2">
2222
- <div>渐变方向</div>
2247
+ <div>{t("panel.config.gradientDirection")}</div>
2223
2248
  <Select
2224
2249
  value={selectedElement.chart?.gradientDirection ?? "to-right"}
2225
2250
  onValueChange={(value) =>
@@ -2233,18 +2258,18 @@ export function PanelConfigSidebar({
2233
2258
  }
2234
2259
  >
2235
2260
  <SelectTrigger className="h-7">
2236
- <SelectValue placeholder="请选择渐变方向" />
2261
+ <SelectValue placeholder={t("panel.config.selectGradientDirection")} />
2237
2262
  </SelectTrigger>
2238
2263
  <SelectContent>
2239
- <SelectItem value="to-right">左 → 右</SelectItem>
2240
- <SelectItem value="to-bottom">上 → 下</SelectItem>
2241
- <SelectItem value="to-bottom-right">左上 → 右下</SelectItem>
2242
- <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>
2243
2268
  </SelectContent>
2244
2269
  </Select>
2245
2270
  </label>
2246
2271
  <div className="col-span-2 space-y-1">
2247
- <div className="text-[11px] text-muted-foreground">渐变预览</div>
2272
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.gradientPreview")}</div>
2248
2273
  <div
2249
2274
  className="h-6 rounded border border-border/60"
2250
2275
  style={{
@@ -2263,7 +2288,7 @@ export function PanelConfigSidebar({
2263
2288
  </div>
2264
2289
  ) : null}
2265
2290
  <label className="block space-y-1.5">
2266
- <div>显示模式</div>
2291
+ <div>{t("panel.config.renderMode")}</div>
2267
2292
  <Select
2268
2293
  value={selectedElement.chart?.renderMode ?? "canvas"}
2269
2294
  onValueChange={(value) =>
@@ -2271,7 +2296,7 @@ export function PanelConfigSidebar({
2271
2296
  }
2272
2297
  >
2273
2298
  <SelectTrigger className="h-7">
2274
- <SelectValue placeholder="请选择显示模式" />
2299
+ <SelectValue placeholder={t("panel.config.selectRenderMode")} />
2275
2300
  </SelectTrigger>
2276
2301
  <SelectContent>
2277
2302
  <SelectItem value="canvas">Canvas</SelectItem>
@@ -2282,7 +2307,7 @@ export function PanelConfigSidebar({
2282
2307
  </>
2283
2308
  )}
2284
2309
  {renderFieldGroup(
2285
- "提示框 Tooltip",
2310
+ t("panel.config.groupTooltip"),
2286
2311
  <>
2287
2312
  <div className="grid grid-cols-2 gap-2">
2288
2313
  <label className="flex items-center gap-2">
@@ -2293,10 +2318,10 @@ export function PanelConfigSidebar({
2293
2318
  updateSelectedChart({ tooltipShow: checked === true })
2294
2319
  }
2295
2320
  />
2296
- <span>显示 Tooltip</span>
2321
+ <span>{t("panel.config.showTooltip")}</span>
2297
2322
  </label>
2298
2323
  <label className="block space-y-1">
2299
- <div>Tooltip 触发方式</div>
2324
+ <div>{t("panel.config.tooltipTrigger")}</div>
2300
2325
  <Select
2301
2326
  value={selectedElement.chart?.tooltipTrigger ?? "axis"}
2302
2327
  onValueChange={(value) =>
@@ -2304,7 +2329,7 @@ export function PanelConfigSidebar({
2304
2329
  }
2305
2330
  >
2306
2331
  <SelectTrigger className="h-7">
2307
- <SelectValue placeholder="请选择触发方式" />
2332
+ <SelectValue placeholder={t("panel.config.selectTooltipTrigger")} />
2308
2333
  </SelectTrigger>
2309
2334
  <SelectContent>
2310
2335
  <SelectItem value="axis">axis</SelectItem>
@@ -2315,34 +2340,34 @@ export function PanelConfigSidebar({
2315
2340
  </div>
2316
2341
  <div className="grid grid-cols-2 gap-2">
2317
2342
  {renderColorField(
2318
- "Tooltip 背景色",
2343
+ t("panel.config.tooltipBgColor"),
2319
2344
  selectedElement.chart?.tooltipBackgroundColor ?? "#0f172a",
2320
2345
  (next) => updateSelectedChart({ tooltipBackgroundColor: next || "#0f172a" })
2321
2346
  )}
2322
2347
  {renderColorField(
2323
- "Tooltip 文字色",
2348
+ t("panel.config.tooltipTextColor"),
2324
2349
  selectedElement.chart?.tooltipTextColor ?? "#f8fafc",
2325
2350
  (next) => updateSelectedChart({ tooltipTextColor: next || "#f8fafc" })
2326
2351
  )}
2327
2352
  </div>
2328
2353
  <label className="block space-y-1">
2329
- {renderFormatterLabel("Tooltip Formatter(可选)")}
2354
+ {renderFormatterLabel(t("panel.config.tooltipFormatter"))}
2330
2355
  <Input
2331
2356
  value={selectedElement.chart?.tooltipFormatter ?? ""}
2332
2357
  onChange={(e) =>
2333
2358
  updateSelectedChart({ tooltipFormatter: e.target.value || undefined })
2334
2359
  }
2335
- placeholder="例如:{b}: {c}"
2360
+ placeholder={t("panel.config.tooltipFormatterPlaceholder")}
2336
2361
  className="h-7"
2337
2362
  />
2338
2363
  </label>
2339
2364
  </>
2340
2365
  )}
2341
2366
  {renderFieldGroup(
2342
- "数据",
2367
+ t("panel.config.groupData"),
2343
2368
  <>
2344
2369
  <label className="block space-y-1">
2345
- <div>类目(逗号分隔)</div>
2370
+ <div>{t("panel.config.labelsCsv")}</div>
2346
2371
  <Input
2347
2372
  value={getChartLabelsDisplayText(selectedElement.chart)}
2348
2373
  onChange={(e) =>
@@ -2355,7 +2380,7 @@ export function PanelConfigSidebar({
2355
2380
  </label>
2356
2381
 
2357
2382
  <label className="block space-y-1">
2358
- <div>数值(逗号分隔)</div>
2383
+ <div>{t("panel.config.valuesCsv")}</div>
2359
2384
  <Input
2360
2385
  value={getChartValuesDisplayText(selectedElement.chart)}
2361
2386
  onChange={(e) =>
@@ -2374,24 +2399,24 @@ export function PanelConfigSidebar({
2374
2399
  selectedChartType === "area" ||
2375
2400
  selectedChartType === "scatter" ? (
2376
2401
  renderFieldGroup(
2377
- "坐标轴",
2402
+ t("panel.config.groupAxes"),
2378
2403
  <>
2379
2404
  <div className="grid grid-cols-2 gap-2">
2380
2405
  <label className="block">
2381
- <div className="mb-1">X 轴名称</div>
2406
+ <div className="mb-1">{t("panel.config.xAxisName")}</div>
2382
2407
  <Input
2383
2408
  value={selectedElement.chart?.xAxisName ?? ""}
2384
2409
  onChange={(e) => updateSelectedChart({ xAxisName: e.target.value })}
2385
- placeholder="例如:日期 / 类目"
2410
+ placeholder={t("panel.config.xAxisNamePlaceholder")}
2386
2411
  className="h-7"
2387
2412
  />
2388
2413
  </label>
2389
2414
  <label className="block">
2390
- <div className="mb-1">Y 轴名称</div>
2415
+ <div className="mb-1">{t("panel.config.yAxisName")}</div>
2391
2416
  <Input
2392
2417
  value={selectedElement.chart?.yAxisName ?? ""}
2393
2418
  onChange={(e) => updateSelectedChart({ yAxisName: e.target.value })}
2394
- placeholder="例如:销量 / 数值"
2419
+ placeholder={t("panel.config.yAxisNamePlaceholder")}
2395
2420
  className="h-7"
2396
2421
  />
2397
2422
  </label>
@@ -2405,7 +2430,7 @@ export function PanelConfigSidebar({
2405
2430
  updateSelectedChart({ xAxisTickShow: checked === true })
2406
2431
  }
2407
2432
  />
2408
- <span>X 轴刻度线</span>
2433
+ <span>{t("panel.config.xAxisTick")}</span>
2409
2434
  </label>
2410
2435
  <label className="flex items-center gap-2">
2411
2436
  <Checkbox
@@ -2415,17 +2440,17 @@ export function PanelConfigSidebar({
2415
2440
  updateSelectedChart({ yAxisTickShow: checked === true })
2416
2441
  }
2417
2442
  />
2418
- <span>Y 轴刻度线</span>
2443
+ <span>{t("panel.config.yAxisTick")}</span>
2419
2444
  </label>
2420
2445
  </div>
2421
2446
  <div className="grid grid-cols-2 gap-2">
2422
2447
  {renderColorField(
2423
- "X 轴刻度线颜色",
2448
+ t("panel.config.xAxisTickColor"),
2424
2449
  selectedElement.chart?.xAxisTickColor ?? "#94a3b8",
2425
2450
  (next) => updateSelectedChart({ xAxisTickColor: next || "#94a3b8" })
2426
2451
  )}
2427
2452
  {renderColorField(
2428
- "Y 轴刻度线颜色",
2453
+ t("panel.config.yAxisTickColor"),
2429
2454
  selectedElement.chart?.yAxisTickColor ?? "#94a3b8",
2430
2455
  (next) => updateSelectedChart({ yAxisTickColor: next || "#94a3b8" })
2431
2456
  )}
@@ -2439,7 +2464,7 @@ export function PanelConfigSidebar({
2439
2464
  updateSelectedChart({ xAxisSplitLineShow: checked === true })
2440
2465
  }
2441
2466
  />
2442
- <span>X 轴分割线</span>
2467
+ <span>{t("panel.config.xAxisSplitLine")}</span>
2443
2468
  </label>
2444
2469
  <label className="flex items-center gap-2">
2445
2470
  <Checkbox
@@ -2449,36 +2474,36 @@ export function PanelConfigSidebar({
2449
2474
  updateSelectedChart({ yAxisSplitLineShow: checked === true })
2450
2475
  }
2451
2476
  />
2452
- <span>Y 轴分割线</span>
2477
+ <span>{t("panel.config.yAxisSplitLine")}</span>
2453
2478
  </label>
2454
2479
  </div>
2455
2480
  <div className="grid grid-cols-2 gap-2">
2456
2481
  {renderColorField(
2457
- "X 轴分割线颜色",
2482
+ t("panel.config.xAxisSplitLineColor"),
2458
2483
  selectedElement.chart?.xAxisSplitLineColor ?? "#e2e8f0",
2459
2484
  (next) => updateSelectedChart({ xAxisSplitLineColor: next || "#e2e8f0" })
2460
2485
  )}
2461
2486
  {renderColorField(
2462
- "Y 轴分割线颜色",
2487
+ t("panel.config.yAxisSplitLineColor"),
2463
2488
  selectedElement.chart?.yAxisSplitLineColor ?? "#e2e8f0",
2464
2489
  (next) => updateSelectedChart({ yAxisSplitLineColor: next || "#e2e8f0" })
2465
2490
  )}
2466
2491
  </div>
2467
2492
  <div className="grid grid-cols-2 gap-2">
2468
2493
  {renderColorField(
2469
- "X 轴标签颜色",
2494
+ t("panel.config.xAxisLabelColor"),
2470
2495
  selectedElement.chart?.xAxisLabelColor ?? "#64748b",
2471
2496
  (next) => updateSelectedChart({ xAxisLabelColor: next || "#64748b" })
2472
2497
  )}
2473
2498
  {renderColorField(
2474
- "Y 轴标签颜色",
2499
+ t("panel.config.yAxisLabelColor"),
2475
2500
  selectedElement.chart?.yAxisLabelColor ?? "#64748b",
2476
2501
  (next) => updateSelectedChart({ yAxisLabelColor: next || "#64748b" })
2477
2502
  )}
2478
2503
  </div>
2479
2504
  <div className="grid grid-cols-2 gap-2">
2480
2505
  <label className="block">
2481
- <div className="mb-1">X 轴标签字号</div>
2506
+ <div className="mb-1">{t("panel.config.xAxisLabelFontSize")}</div>
2482
2507
  <Input
2483
2508
  type="number"
2484
2509
  min={8}
@@ -2493,7 +2518,7 @@ export function PanelConfigSidebar({
2493
2518
  />
2494
2519
  </label>
2495
2520
  <label className="block">
2496
- <div className="mb-1">Y 轴标签字号</div>
2521
+ <div className="mb-1">{t("panel.config.yAxisLabelFontSize")}</div>
2497
2522
  <Input
2498
2523
  type="number"
2499
2524
  min={8}
@@ -2517,7 +2542,7 @@ export function PanelConfigSidebar({
2517
2542
  updateSelectedChart({ xAxisLabelAutoEllipsis: checked === true })
2518
2543
  }
2519
2544
  />
2520
- <span>X 轴标签自动缩略</span>
2545
+ <span>{t("panel.config.xAxisLabelAutoEllipsis")}</span>
2521
2546
  </label>
2522
2547
  <label className="flex items-center gap-2">
2523
2548
  <Checkbox
@@ -2527,7 +2552,7 @@ export function PanelConfigSidebar({
2527
2552
  updateSelectedChart({ yAxisLabelAutoEllipsis: checked === true })
2528
2553
  }
2529
2554
  />
2530
- <span>Y 轴标签自动缩略</span>
2555
+ <span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
2531
2556
  </label>
2532
2557
  </div>
2533
2558
  </>
@@ -2539,11 +2564,11 @@ export function PanelConfigSidebar({
2539
2564
  selectedChartType === "area" ||
2540
2565
  selectedChartType === "pie" ? (
2541
2566
  renderFieldGroup(
2542
- "系列",
2567
+ t("panel.config.groupSeries"),
2543
2568
  <>
2544
2569
  {selectedChartType === "bar" ? (
2545
2570
  <label className="block space-y-1">
2546
- <div>柱宽(px)</div>
2571
+ <div>{t("panel.config.barWidthPx")}</div>
2547
2572
  <Input
2548
2573
  type="number"
2549
2574
  min={1}
@@ -2565,14 +2590,14 @@ export function PanelConfigSidebar({
2565
2590
  updateSelectedChart({ smooth: checked === true })
2566
2591
  }
2567
2592
  />
2568
- <span>平滑曲线</span>
2593
+ <span>{t("panel.config.smooth")}</span>
2569
2594
  </label>
2570
2595
  ) : null}
2571
2596
 
2572
2597
  {selectedChartType === "pie" ? (
2573
2598
  <div className="grid grid-cols-2 gap-2">
2574
2599
  <label className="block space-y-1">
2575
- <div>内半径(%)</div>
2600
+ <div>{t("panel.config.pieInnerRadiusPct")}</div>
2576
2601
  <Input
2577
2602
  type="number"
2578
2603
  min={0}
@@ -2590,7 +2615,7 @@ export function PanelConfigSidebar({
2590
2615
  />
2591
2616
  </label>
2592
2617
  <label className="block space-y-1">
2593
- <div>外半径(%)</div>
2618
+ <div>{t("panel.config.pieOuterRadiusPct")}</div>
2594
2619
  <Input
2595
2620
  type="number"
2596
2621
  min={1}
@@ -2615,15 +2640,15 @@ export function PanelConfigSidebar({
2615
2640
  </>,
2616
2641
  true,
2617
2642
  [
2618
- "图表",
2643
+ t("panel.config.searchKwChart"),
2619
2644
  "title",
2620
2645
  "tooltip",
2621
- "x轴",
2622
- "y轴",
2646
+ t("panel.config.searchKwXAxis"),
2647
+ t("panel.config.searchKwYAxis"),
2623
2648
  "axis",
2624
2649
  "label",
2625
- "刻度线",
2626
- "分割线",
2650
+ t("panel.config.searchKwTick"),
2651
+ t("panel.config.searchKwSplit"),
2627
2652
  "render",
2628
2653
  "svg",
2629
2654
  "canvas",
@@ -2632,10 +2657,10 @@ export function PanelConfigSidebar({
2632
2657
 
2633
2658
  {renderSection(
2634
2659
  "chartAdvanced",
2635
- "图表配置 / 高级",
2660
+ t("panel.config.sectionChartAdvanced"),
2636
2661
  <>
2637
2662
  {renderFieldGroup(
2638
- "常用项(表单)",
2663
+ t("panel.config.groupCommonForm"),
2639
2664
  <div className="grid grid-cols-1 gap-2">
2640
2665
  <div className="rounded-lg border border-border/60 bg-background/70 p-3">
2641
2666
  <Collapsible
@@ -2647,75 +2672,75 @@ export function PanelConfigSidebar({
2647
2672
  <button
2648
2673
  type="button"
2649
2674
  className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
2650
- aria-label={isSectionExpanded("chartAdvancedLayout", false) ? "收起布局与坐标" : "展开布局与坐标"}
2675
+ aria-label={isSectionExpanded("chartAdvancedLayout", false) ? t("panel.config.collapseLayoutCoord") : t("panel.config.expandLayoutCoord")}
2651
2676
  >
2652
2677
  {isSectionExpanded("chartAdvancedLayout", false) ? "▾" : "▸"}
2653
2678
  </button>
2654
2679
  </CollapsibleTrigger>
2655
- <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>
2656
2681
  </div>
2657
2682
  <CollapsibleContent>
2658
2683
  <div className="grid grid-cols-2 gap-2">
2659
2684
  <label className="block space-y-1.5">
2660
- <div className={optionLabelTextClass}>网格左距(grid.left)</div>
2685
+ <div className={optionLabelTextClass}>{t("panel.config.gridLeftForm")}</div>
2661
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 } })} />
2662
2687
  </label>
2663
2688
  <label className="block space-y-1.5">
2664
- <div className={optionLabelTextClass}>网格右距(grid.right)</div>
2689
+ <div className={optionLabelTextClass}>{t("panel.config.gridRightForm")}</div>
2665
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 } })} />
2666
2691
  </label>
2667
2692
  <label className="block space-y-1.5">
2668
- <div className={optionLabelTextClass}>网格上距(grid.top)</div>
2693
+ <div className={optionLabelTextClass}>{t("panel.config.gridTopForm")}</div>
2669
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 } })} />
2670
2695
  </label>
2671
2696
  <label className="block space-y-1.5">
2672
- <div className={optionLabelTextClass}>网格下距(grid.bottom)</div>
2697
+ <div className={optionLabelTextClass}>{t("panel.config.gridBottomForm")}</div>
2673
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 } })} />
2674
2699
  </label>
2675
2700
  <label className="block space-y-1.5">
2676
- <div className={optionLabelTextClass}>图例位置(legend.top)</div>
2701
+ <div className={optionLabelTextClass}>{t("panel.config.legendPositionForm")}</div>
2677
2702
  <Select value={String((selectedElement.chart?.option as any)?.legend?.top ?? "top")} onValueChange={(value) => updateSelectedOptionForm({ legend: { top: value } })}>
2678
- <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder="选择图例位置" /></SelectTrigger>
2703
+ <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectLegendPosition")} /></SelectTrigger>
2679
2704
  <SelectContent>
2680
- <SelectItem value="top">顶部</SelectItem>
2681
- <SelectItem value="bottom">底部</SelectItem>
2682
- <SelectItem value="left">左侧</SelectItem>
2683
- <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>
2684
2709
  </SelectContent>
2685
2710
  </Select>
2686
2711
  </label>
2687
2712
  <label className="block space-y-1.5">
2688
- <div className={optionLabelTextClass}>图例排列(legend.orient)</div>
2713
+ <div className={optionLabelTextClass}>{t("panel.config.legendOrientForm")}</div>
2689
2714
  <Select value={String((selectedElement.chart?.option as any)?.legend?.orient ?? "horizontal")} onValueChange={(value) => updateSelectedOptionForm({ legend: { orient: value } })}>
2690
- <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder="选择图例排列" /></SelectTrigger>
2715
+ <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectLegendOrient")} /></SelectTrigger>
2691
2716
  <SelectContent>
2692
- <SelectItem value="horizontal">横向</SelectItem>
2693
- <SelectItem value="vertical">纵向</SelectItem>
2717
+ <SelectItem value="horizontal">{t("panel.config.legendHorizontal")}</SelectItem>
2718
+ <SelectItem value="vertical">{t("panel.config.legendVertical")}</SelectItem>
2694
2719
  </SelectContent>
2695
2720
  </Select>
2696
2721
  </label>
2697
2722
  <label className="block space-y-1.5">
2698
- {renderOptionLabel("X 轴最小值", "xAxis.min", "设置横轴最小值,留空可在 JSON 里删除。")}
2723
+ {renderOptionLabel(t("panel.config.xAxisMin"), "xAxis.min", t("panel.config.xAxisMinHint"))}
2699
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 } })} />
2700
2725
  </label>
2701
2726
  <label className="block space-y-1.5">
2702
- {renderOptionLabel("X 轴最大值", "xAxis.max", "设置横轴最大值,留空可在 JSON 里删除。")}
2727
+ {renderOptionLabel(t("panel.config.xAxisMax"), "xAxis.max", t("panel.config.xAxisMaxHint"))}
2703
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 } })} />
2704
2729
  </label>
2705
2730
  <label className="block space-y-1.5">
2706
- {renderOptionLabel("Y 轴最小值", "yAxis.min", "设置纵轴最小值,留空可在 JSON 里删除。")}
2731
+ {renderOptionLabel(t("panel.config.yAxisMin"), "yAxis.min", t("panel.config.yAxisMinHint"))}
2707
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 } })} />
2708
2733
  </label>
2709
2734
  <label className="block space-y-1.5">
2710
- {renderOptionLabel("Y 轴最大值", "yAxis.max", "设置纵轴最大值,留空可在 JSON 里删除。")}
2735
+ {renderOptionLabel(t("panel.config.yAxisMax"), "yAxis.max", t("panel.config.yAxisMaxHint"))}
2711
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 } })} />
2712
2737
  </label>
2713
2738
  <label className="block space-y-1.5">
2714
- {renderOptionLabel("X 轴标签旋转", "xAxis.axisLabel.rotate", "单位为度。")}
2739
+ {renderOptionLabel(t("panel.config.xAxisLabelRotate"), "xAxis.axisLabel.rotate", t("panel.config.rotateUnitHint"))}
2715
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 } } })} />
2716
2741
  </label>
2717
2742
  <label className="block space-y-1.5">
2718
- {renderOptionLabel("Y 轴标签旋转", "yAxis.axisLabel.rotate", "单位为度。")}
2743
+ {renderOptionLabel(t("panel.config.yAxisLabelRotate"), "yAxis.axisLabel.rotate", t("panel.config.rotateUnitHint"))}
2719
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 } } })} />
2720
2745
  </label>
2721
2746
  </div>
@@ -2733,37 +2758,37 @@ export function PanelConfigSidebar({
2733
2758
  <button
2734
2759
  type="button"
2735
2760
  className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
2736
- aria-label={isSectionExpanded("chartAdvancedHighFreq", true) ? "收起高频项" : "展开高频项"}
2761
+ aria-label={isSectionExpanded("chartAdvancedHighFreq", true) ? t("panel.config.collapseHighFreq") : t("panel.config.expandHighFreq")}
2737
2762
  >
2738
2763
  {isSectionExpanded("chartAdvancedHighFreq", true) ? "▾" : "▸"}
2739
2764
  </button>
2740
2765
  </CollapsibleTrigger>
2741
- <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>
2742
2767
  </div>
2743
2768
  <CollapsibleContent>
2744
2769
  <div className="grid grid-cols-1 gap-2">
2745
2770
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2746
2771
  <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.legend?.show ?? true)} onCheckedChange={(checked) => updateSelectedOptionForm({ legend: { show: checked === true } })} />
2747
- {renderOptionLabel("显示图例", "legend.show", "控制图例显隐。")}
2772
+ {renderOptionLabel(t("panel.config.showLegend"), "legend.show", t("panel.config.showLegendHint"))}
2748
2773
  </label>
2749
2774
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2750
2775
  <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.grid?.containLabel ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ grid: { containLabel: checked === true } })} />
2751
- {renderOptionLabel("网格包含标签", "grid.containLabel", "自动为坐标轴标签预留空间。")}
2776
+ {renderOptionLabel(t("panel.config.gridContainLabel"), "grid.containLabel", t("panel.config.gridContainLabelHint"))}
2752
2777
  </label>
2753
2778
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2754
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 }); }} />
2755
- {renderOptionLabel("内置缩放", "dataZoom[type=inside]", "启用鼠标滚轮/手势缩放。")}
2780
+ {renderOptionLabel(t("panel.config.zoomInside"), "dataZoom[type=inside]", t("panel.config.zoomInsideHint"))}
2756
2781
  </label>
2757
2782
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2758
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 }); }} />
2759
- {renderOptionLabel("滑块缩放", "dataZoom[type=slider]", "显示底部拖拽缩放条。")}
2784
+ {renderOptionLabel(t("panel.config.zoomSlider"), "dataZoom[type=slider]", t("panel.config.zoomSliderHint"))}
2760
2785
  </label>
2761
2786
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2762
2787
  <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.animation ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ animation: checked === true })} />
2763
- {renderOptionLabel("开启动画", "animation", "开启后切换数据时有过渡动效。")}
2788
+ {renderOptionLabel(t("panel.config.enableAnimation"), "animation", t("panel.config.enableAnimationHint"))}
2764
2789
  </label>
2765
2790
  <label className="block space-y-1.5">
2766
- {renderOptionLabel("动画时长", "animationDuration", "动画持续时间,单位毫秒。")}
2791
+ {renderOptionLabel(t("panel.config.animationDuration"), "animationDuration", t("panel.config.animationDurationHint"))}
2767
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) })} />
2768
2793
  </label>
2769
2794
  </div>
@@ -2778,33 +2803,33 @@ export function PanelConfigSidebar({
2778
2803
  <button
2779
2804
  type="button"
2780
2805
  className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
2781
- aria-label={isSectionExpanded("chartAdvancedAxisPointer", false) ? "收起轴指示器项" : "展开轴指示器项"}
2806
+ aria-label={isSectionExpanded("chartAdvancedAxisPointer", false) ? t("panel.config.collapseAxisPointer") : t("panel.config.expandAxisPointer")}
2782
2807
  >
2783
2808
  {isSectionExpanded("chartAdvancedAxisPointer", false) ? "▾" : "▸"}
2784
2809
  </button>
2785
2810
  </CollapsibleTrigger>
2786
- <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>
2787
2812
  </div>
2788
2813
  <CollapsibleContent>
2789
2814
  <div className="grid grid-cols-1 gap-2">
2790
2815
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2791
2816
  <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.show ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { show: checked === true } })} />
2792
- {renderOptionLabel("显示轴指示器", "axisPointer.show", "悬停时显示轴对齐提示。")}
2817
+ {renderOptionLabel(t("panel.config.showAxisPointer"), "axisPointer.show", t("panel.config.showAxisPointerHint"))}
2793
2818
  </label>
2794
2819
  <label className="block space-y-1.5 rounded-md bg-muted/30 px-2 py-1.5">
2795
- {renderOptionLabel("轴指示器类型", "axisPointer.type", "设置指示器样式:线/阴影/十字。")}
2820
+ {renderOptionLabel(t("panel.config.axisPointerType"), "axisPointer.type", t("panel.config.axisPointerTypeHint"))}
2796
2821
  <Select value={String((selectedElement.chart?.option as any)?.axisPointer?.type ?? "line")} onValueChange={(value) => updateSelectedOptionForm({ axisPointer: { type: value } })}>
2797
- <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder="选择轴指示器类型" /></SelectTrigger>
2822
+ <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectAxisPointerType")} /></SelectTrigger>
2798
2823
  <SelectContent>
2799
- <SelectItem value="line">线</SelectItem>
2800
- <SelectItem value="shadow">阴影</SelectItem>
2801
- <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>
2802
2827
  </SelectContent>
2803
2828
  </Select>
2804
2829
  </label>
2805
2830
  <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2806
2831
  <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.snap ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { snap: checked === true } })} />
2807
- {renderOptionLabel("轴指示器吸附", "axisPointer.snap", "指示器吸附到最近数据点。")}
2832
+ {renderOptionLabel(t("panel.config.axisPointerSnap"), "axisPointer.snap", t("panel.config.axisPointerSnapHint"))}
2808
2833
  </label>
2809
2834
  </div>
2810
2835
  </CollapsibleContent>
@@ -2814,7 +2839,7 @@ export function PanelConfigSidebar({
2814
2839
  </div>
2815
2840
  )}
2816
2841
  {renderFieldGroup(
2817
- "JSON 高级模式",
2842
+ t("panel.config.groupJsonAdvanced"),
2818
2843
  <>
2819
2844
  <label className="flex items-center gap-2 rounded-md bg-background/70 px-2 py-1.5">
2820
2845
  <Checkbox
@@ -2822,9 +2847,9 @@ export function PanelConfigSidebar({
2822
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"
2823
2848
  onCheckedChange={(checked) => setIsAdvancedOptionMode(checked === true)}
2824
2849
  />
2825
- <span>开启高级模式(直接编辑图表 JSON 配置)</span>
2826
- <ConfigHintIcon label="图表 JSON 高级模式">
2827
- 基础配置会先生成图表配置,高级模式会在此基础上覆盖(深度合并)。
2850
+ <span>{t("panel.config.enableAdvancedJson")}</span>
2851
+ <ConfigHintIcon label={t("panel.config.advancedJsonHintLabel")}>
2852
+ {t("panel.config.advancedJsonHint")}
2828
2853
  </ConfigHintIcon>
2829
2854
  </label>
2830
2855
  {isAdvancedOptionMode ? (
@@ -2839,7 +2864,7 @@ export function PanelConfigSidebar({
2839
2864
  updateSelectedChart({ option: parsed });
2840
2865
  setOptionJsonError(null);
2841
2866
  } catch {
2842
- setOptionJsonError("JSON 格式错误,修正后会自动应用");
2867
+ setOptionJsonError(t("panel.config.jsonInvalid"));
2843
2868
  }
2844
2869
  }}
2845
2870
  spellCheck={false}
@@ -2851,7 +2876,7 @@ export function PanelConfigSidebar({
2851
2876
  </div>
2852
2877
  ) : (
2853
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">
2854
- JSON 有效,已实时应用到当前图表。
2879
+ {t("panel.config.jsonValidApplied")}
2855
2880
  </div>
2856
2881
  )}
2857
2882
  </>
@@ -2860,16 +2885,35 @@ export function PanelConfigSidebar({
2860
2885
  )}
2861
2886
  </>,
2862
2887
  false,
2863
- ["json", "option", "高级", "echarts"]
2888
+ ["json", "option", t("panel.config.searchKwAdvanced"), "echarts"]
2864
2889
  )}
2865
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
+ )
2866
2910
  ) : selectedElement.materialType === "text" ? (
2867
2911
  renderSection(
2868
2912
  "textConfig",
2869
- "文本配置",
2913
+ t("panel.config.sectionText"),
2870
2914
  <>
2871
2915
  {renderFieldGroup(
2872
- "文本内容",
2916
+ t("panel.config.groupTextContent"),
2873
2917
  <>
2874
2918
  <div className="flex items-center gap-1">
2875
2919
  <button
@@ -2896,6 +2940,7 @@ export function PanelConfigSidebar({
2896
2940
  </div>
2897
2941
  <div
2898
2942
  ref={textEditorRef}
2943
+ data-config-field="textHtml"
2899
2944
  className="min-h-[120px] rounded border border-border bg-background px-2 py-1.5 text-xs leading-6 outline-none"
2900
2945
  style={{
2901
2946
  fontFamily: selectedElement.textFontFamily || undefined,
@@ -2921,10 +2966,10 @@ export function PanelConfigSidebar({
2921
2966
  </>
2922
2967
  )}
2923
2968
  {renderFieldGroup(
2924
- "文字样式",
2969
+ t("panel.config.groupTextStyle"),
2925
2970
  <>
2926
2971
  <label className="block space-y-1">
2927
- <div>字体</div>
2972
+ <div>{t("panel.config.fontFamily")}</div>
2928
2973
  <Input
2929
2974
  value={selectedElement.textFontFamily ?? ""}
2930
2975
  onChange={(e) =>
@@ -2932,13 +2977,13 @@ export function PanelConfigSidebar({
2932
2977
  textFontFamily: e.target.value || undefined,
2933
2978
  })
2934
2979
  }
2935
- placeholder="如:Inter, PingFang SC, Microsoft YaHei"
2980
+ placeholder={t("panel.config.fontFamilyPlaceholder")}
2936
2981
  className="h-7"
2937
2982
  />
2938
2983
  </label>
2939
2984
  <div className="grid grid-cols-2 gap-2">
2940
2985
  <label className="block space-y-1">
2941
- <div>字号(px)</div>
2986
+ <div>{t("panel.config.fontSizePx")}</div>
2942
2987
  <Input
2943
2988
  type="number"
2944
2989
  min={8}
@@ -2954,7 +2999,7 @@ export function PanelConfigSidebar({
2954
2999
  />
2955
3000
  </label>
2956
3001
  <label className="block space-y-1">
2957
- <div>字重</div>
3002
+ <div>{t("panel.config.fontWeight")}</div>
2958
3003
  <Select
2959
3004
  value={selectedElement.textFontWeight ?? "400"}
2960
3005
  onValueChange={(value) =>
@@ -2964,7 +3009,7 @@ export function PanelConfigSidebar({
2964
3009
  }
2965
3010
  >
2966
3011
  <SelectTrigger className="h-7">
2967
- <SelectValue placeholder="选择字重" />
3012
+ <SelectValue placeholder={t("panel.config.selectFontWeight")} />
2968
3013
  </SelectTrigger>
2969
3014
  <SelectContent>
2970
3015
  <SelectItem value="300">300</SelectItem>
@@ -2978,7 +3023,7 @@ export function PanelConfigSidebar({
2978
3023
  </div>
2979
3024
  <div className="grid grid-cols-2 gap-2">
2980
3025
  <label className="block space-y-1">
2981
- <div>对齐</div>
3026
+ <div>{t("panel.config.textAlign")}</div>
2982
3027
  <Select
2983
3028
  value={selectedElement.textAlign ?? "left"}
2984
3029
  onValueChange={(value) =>
@@ -2988,18 +3033,18 @@ export function PanelConfigSidebar({
2988
3033
  }
2989
3034
  >
2990
3035
  <SelectTrigger className="h-7">
2991
- <SelectValue placeholder="选择对齐方式" />
3036
+ <SelectValue placeholder={t("panel.config.selectTextAlign")} />
2992
3037
  </SelectTrigger>
2993
3038
  <SelectContent>
2994
- <SelectItem value="left">左对齐</SelectItem>
2995
- <SelectItem value="center">居中</SelectItem>
2996
- <SelectItem value="right">右对齐</SelectItem>
2997
- <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>
2998
3043
  </SelectContent>
2999
3044
  </Select>
3000
3045
  </label>
3001
3046
  <label className="block space-y-1">
3002
- <div>行高</div>
3047
+ <div>{t("panel.config.lineHeight")}</div>
3003
3048
  <Input
3004
3049
  type="number"
3005
3050
  min={1}
@@ -3019,7 +3064,7 @@ export function PanelConfigSidebar({
3019
3064
  </label>
3020
3065
  </div>
3021
3066
  {renderColorField(
3022
- "文字颜色",
3067
+ t("panel.config.textColor"),
3023
3068
  selectedElement.textColor ?? "",
3024
3069
  (next) =>
3025
3070
  updateSelectedText({
@@ -3029,7 +3074,7 @@ export function PanelConfigSidebar({
3029
3074
  </>
3030
3075
  )}
3031
3076
  {renderFieldGroup(
3032
- "输入能力",
3077
+ t("panel.config.groupInputAbility"),
3033
3078
  <label className="flex items-center gap-2">
3034
3079
  <Checkbox
3035
3080
  checked={selectedElement.textAllowInput ?? true}
@@ -3040,23 +3085,23 @@ export function PanelConfigSidebar({
3040
3085
  })
3041
3086
  }
3042
3087
  />
3043
- <span>允许在画布内直接输入(默认开启)</span>
3088
+ <span>{t("panel.config.allowCanvasInput")}</span>
3044
3089
  </label>
3045
3090
  )}
3046
3091
  </>,
3047
3092
  true,
3048
- ["文本", "富文本", "字体", "颜色", "对齐", "行高", "输入"]
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")]
3049
3094
  )
3050
3095
  ) : selectedElement.materialType === "audio" ? (
3051
3096
  renderSection(
3052
3097
  "audioConfig",
3053
- "音频配置",
3098
+ t("panel.config.sectionAudio"),
3054
3099
  <>
3055
3100
  {renderFieldGroup(
3056
- "音频来源",
3101
+ t("panel.config.groupAudioSource"),
3057
3102
  <>
3058
3103
  <label className="block space-y-1">
3059
- <div>音频 URL</div>
3104
+ <div>{t("panel.config.audioUrl")}</div>
3060
3105
  <Input
3061
3106
  value={selectedElement.audioRemoteUrl ?? ""}
3062
3107
  onChange={(e) =>
@@ -3071,7 +3116,7 @@ export function PanelConfigSidebar({
3071
3116
  </label>
3072
3117
  <div className="flex items-center gap-2">
3073
3118
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3074
- 上传音频
3119
+ {t("panel.config.uploadAudio")}
3075
3120
  <Input
3076
3121
  type="file"
3077
3122
  accept="audio/*"
@@ -3089,7 +3134,7 @@ export function PanelConfigSidebar({
3089
3134
  className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent disabled:opacity-50"
3090
3135
  onClick={isRecordingAudio ? stopRecordingAudio : startRecordingAudio}
3091
3136
  >
3092
- {isRecordingAudio ? "停止录音" : "开始录音"}
3137
+ {isRecordingAudio ? t("panel.config.stopRecord") : t("panel.config.startRecord")}
3093
3138
  </button>
3094
3139
  </div>
3095
3140
  {audioStatus ? (
@@ -3105,7 +3150,7 @@ export function PanelConfigSidebar({
3105
3150
  </>
3106
3151
  )}
3107
3152
  {renderFieldGroup(
3108
- "展示样式",
3153
+ t("panel.config.groupDisplayStyle"),
3109
3154
  <>
3110
3155
  <label className="flex items-center gap-2">
3111
3156
  <Checkbox
@@ -3117,10 +3162,10 @@ export function PanelConfigSidebar({
3117
3162
  })
3118
3163
  }
3119
3164
  />
3120
- <span>编辑时自动暂停媒体</span>
3165
+ <span>{t("panel.config.autoPauseMedia")}</span>
3121
3166
  </label>
3122
3167
  <label className="block space-y-1">
3123
- <div>预设喇叭图标</div>
3168
+ <div>{t("panel.config.audioIconPreset")}</div>
3124
3169
  <Select
3125
3170
  value={selectedElement.audioIconPreset ?? "__none__"}
3126
3171
  onValueChange={(value) =>
@@ -3133,19 +3178,19 @@ export function PanelConfigSidebar({
3133
3178
  }
3134
3179
  >
3135
3180
  <SelectTrigger className="h-7">
3136
- <SelectValue placeholder="选择图标" />
3181
+ <SelectValue placeholder={t("panel.config.selectIcon")} />
3137
3182
  </SelectTrigger>
3138
3183
  <SelectContent>
3139
- <SelectItem value="__none__">默认(显示进度条)</SelectItem>
3140
- <SelectItem value="speaker">喇叭</SelectItem>
3141
- <SelectItem value="music">音符</SelectItem>
3142
- <SelectItem value="headphone">耳机</SelectItem>
3143
- <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>
3144
3189
  </SelectContent>
3145
3190
  </Select>
3146
3191
  </label>
3147
3192
  <label className="block space-y-1">
3148
- <div>播放动效</div>
3193
+ <div>{t("panel.config.visualEffect")}</div>
3149
3194
  <Select
3150
3195
  value={selectedElement.audioVisualEffect ?? "pulse"}
3151
3196
  onValueChange={(value) =>
@@ -3155,17 +3200,17 @@ export function PanelConfigSidebar({
3155
3200
  }
3156
3201
  >
3157
3202
  <SelectTrigger className="h-7">
3158
- <SelectValue placeholder="选择动效" />
3203
+ <SelectValue placeholder={t("panel.config.selectEffect")} />
3159
3204
  </SelectTrigger>
3160
3205
  <SelectContent>
3161
- <SelectItem value="none">无动效</SelectItem>
3162
- <SelectItem value="pulse">呼吸高亮</SelectItem>
3163
- <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>
3164
3209
  </SelectContent>
3165
3210
  </Select>
3166
3211
  </label>
3167
3212
  <label className="block space-y-1">
3168
- <div>动效速度</div>
3213
+ <div>{t("panel.config.effectSpeed")}</div>
3169
3214
  <Select
3170
3215
  value={selectedElement.audioVisualSpeed ?? "normal"}
3171
3216
  onValueChange={(value) =>
@@ -3175,18 +3220,18 @@ export function PanelConfigSidebar({
3175
3220
  }
3176
3221
  >
3177
3222
  <SelectTrigger className="h-7">
3178
- <SelectValue placeholder="选择速度" />
3223
+ <SelectValue placeholder={t("panel.config.selectSpeed")} />
3179
3224
  </SelectTrigger>
3180
3225
  <SelectContent>
3181
- <SelectItem value="slow">慢</SelectItem>
3182
- <SelectItem value="normal">中</SelectItem>
3183
- <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>
3184
3229
  </SelectContent>
3185
3230
  </Select>
3186
3231
  </label>
3187
3232
  <div className="flex items-center gap-2">
3188
3233
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3189
- 上传占位图
3234
+ {t("panel.config.uploadPoster")}
3190
3235
  <Input
3191
3236
  type="file"
3192
3237
  accept="image/*"
@@ -3208,38 +3253,38 @@ export function PanelConfigSidebar({
3208
3253
  })
3209
3254
  }
3210
3255
  >
3211
- 清空占位图
3256
+ {t("panel.config.clearPoster")}
3212
3257
  </button>
3213
3258
  </div>
3214
3259
  {selectedElement.audioPosterImage ? (
3215
3260
  <img
3216
3261
  src={selectedElement.audioPosterImage}
3217
- alt="音频占位图预览"
3262
+ alt={t("panel.config.audioPosterAlt")}
3218
3263
  className="h-20 w-full rounded border border-border/60 object-cover"
3219
3264
  />
3220
3265
  ) : null}
3221
3266
  <div className="flex items-center gap-1">
3222
- <div className="text-[11px] text-muted-foreground">音频占位图</div>
3223
- <ConfigHintIcon label="音频占位图">
3224
- 设置占位图或图标后,节点上将隐藏进度条,改为点击图标播放/暂停。
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")}
3225
3270
  </ConfigHintIcon>
3226
3271
  </div>
3227
3272
  </>
3228
3273
  )}
3229
3274
  </>,
3230
3275
  true,
3231
- ["音频", "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"]
3232
3277
  )
3233
3278
  ) : selectedElement.materialType === "video" ? (
3234
3279
  renderSection(
3235
3280
  "videoConfig",
3236
- "视频配置",
3281
+ t("panel.config.sectionVideo"),
3237
3282
  <>
3238
3283
  {renderFieldGroup(
3239
- "视频来源",
3284
+ t("panel.config.groupVideoSource"),
3240
3285
  <>
3241
3286
  <label className="block space-y-1">
3242
- <div>视频 URL</div>
3287
+ <div>{t("panel.config.videoUrl")}</div>
3243
3288
  <Input
3244
3289
  value={selectedElement.videoRemoteUrl ?? ""}
3245
3290
  onChange={(e) =>
@@ -3254,7 +3299,7 @@ export function PanelConfigSidebar({
3254
3299
  </label>
3255
3300
  <div className="flex items-center gap-2">
3256
3301
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3257
- 上传视频
3302
+ {t("panel.config.uploadVideo")}
3258
3303
  <Input
3259
3304
  type="file"
3260
3305
  accept="video/*"
@@ -3288,26 +3333,26 @@ export function PanelConfigSidebar({
3288
3333
  })
3289
3334
  }
3290
3335
  />
3291
- <span>编辑时自动暂停媒体</span>
3336
+ <span>{t("panel.config.autoPauseMedia")}</span>
3292
3337
  </label>
3293
3338
  </>
3294
3339
  )}
3295
3340
  </>,
3296
3341
  true,
3297
- ["视频", "url", "上传", "预览", "自动暂停", "media"]
3342
+ [t("panel.material.video"), "url", t("panel.config.searchKwUpload"), t("panel.config.searchKwPreview"), t("panel.config.searchKwAutoPause"), "media"]
3298
3343
  )
3299
3344
  ) : null}
3300
3345
  {selectedElement
3301
3346
  ? selectedElement.parentGridId
3302
3347
  ? renderSection(
3303
3348
  "gridChildSpan",
3304
- "网格子节点占位",
3349
+ t("panel.config.sectionGridChildSpan"),
3305
3350
  <>
3306
3351
  {renderFieldGroup(
3307
- "跨槽位",
3352
+ t("panel.config.groupCrossSlots"),
3308
3353
  <div className="grid grid-cols-2 gap-2">
3309
3354
  <label className="block space-y-1">
3310
- <div>跨列(colSpan)</div>
3355
+ <div>{t("panel.config.colSpan")}</div>
3311
3356
  <Input
3312
3357
  type="number"
3313
3358
  min={1}
@@ -3322,7 +3367,7 @@ export function PanelConfigSidebar({
3322
3367
  />
3323
3368
  </label>
3324
3369
  <label className="block space-y-1">
3325
- <div>跨行(rowSpan)</div>
3370
+ <div>{t("panel.config.rowSpan")}</div>
3326
3371
  <Input
3327
3372
  type="number"
3328
3373
  min={1}
@@ -3340,21 +3385,21 @@ export function PanelConfigSidebar({
3340
3385
  )}
3341
3386
  </>,
3342
3387
  true,
3343
- ["网格", "跨列", "跨行", "span", "slot"],
3344
- <>网格子节点可跨越多格,占据更大区域,便于复杂布局。</>
3388
+ [t("panel.config.searchKwGrid"), t("panel.config.searchKwCrossCol"), t("panel.config.searchKwCrossRow"), "span", "slot"],
3389
+ <>{t("panel.config.gridChildHint")}</>
3345
3390
  )
3346
3391
  : null
3347
3392
  : null}
3348
3393
  {selectedElement.materialType === "geometry" ? (
3349
3394
  renderSection(
3350
3395
  "geometryConfig",
3351
- "几何配置",
3396
+ t("panel.config.sectionGeometry"),
3352
3397
  <>
3353
3398
  {renderFieldGroup(
3354
- "基础形状",
3399
+ t("panel.config.groupBasicShape"),
3355
3400
  <>
3356
3401
  <label className="block space-y-1">
3357
- <div>形状</div>
3402
+ <div>{t("panel.config.shape")}</div>
3358
3403
  <Select
3359
3404
  value={selectedElement.geometryShape ?? "rect"}
3360
3405
  onValueChange={(value) =>
@@ -3364,47 +3409,47 @@ export function PanelConfigSidebar({
3364
3409
  }
3365
3410
  >
3366
3411
  <SelectTrigger className="h-7">
3367
- <SelectValue placeholder="选择形状" />
3412
+ <SelectValue placeholder={t("panel.config.selectShape")} />
3368
3413
  </SelectTrigger>
3369
3414
  <SelectContent>
3370
- <SelectItem value="rect">矩形</SelectItem>
3371
- <SelectItem value="circle">圆形</SelectItem>
3372
- <SelectItem value="triangle">三角形</SelectItem>
3373
- <SelectItem value="diamond">菱形</SelectItem>
3374
- <SelectItem value="hexagon">六边形</SelectItem>
3375
- <SelectItem value="star">星形</SelectItem>
3376
- <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>
3377
3422
  </SelectContent>
3378
3423
  </Select>
3379
3424
  </label>
3380
3425
  {renderColorField(
3381
- "几何颜色",
3426
+ t("panel.config.geometryColor"),
3382
3427
  selectedElement.geometryColor ?? "#3b82f6",
3383
3428
  (next) => updateSelectedGeometry({ geometryColor: next || "#3b82f6" })
3384
3429
  )}
3385
3430
  </>
3386
3431
  )}
3387
3432
  {renderFieldGroup(
3388
- "高级(Canvas 脚本)",
3433
+ t("panel.config.groupCanvasScript"),
3389
3434
  <>
3390
3435
  <Textarea
3391
3436
  value={selectedElement.geometryScript ?? ""}
3392
3437
  onChange={(e) => updateSelectedGeometry({ geometryScript: e.target.value || undefined })}
3393
3438
  spellCheck={false}
3394
3439
  className="h-36 font-mono text-[11px]"
3395
- placeholder="// 例: ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.fillRect(8,8,width-16,height-16);"
3440
+ placeholder={t("panel.config.geometryScriptPlaceholder")}
3396
3441
  />
3397
3442
  </>,
3398
3443
  <>
3399
- 可输入 Canvas 绘制逻辑,变量:ctx、width、height、element。脚本异常会被安全忽略。
3444
+ {t("panel.config.canvasScriptHint")}
3400
3445
  </>
3401
3446
  )}
3402
3447
  {renderFieldGroup(
3403
- "手绘叠加",
3448
+ t("panel.config.groupSketchOverlay"),
3404
3449
  <>
3405
3450
  <div className="flex items-center gap-2">
3406
3451
  <label className="block space-y-1">
3407
- <div className="text-[11px]">画笔颜色</div>
3452
+ <div className="text-[11px]">{t("panel.config.penColor")}</div>
3408
3453
  <Input
3409
3454
  type="color"
3410
3455
  value={geometryDrawPenColor}
@@ -3413,7 +3458,7 @@ export function PanelConfigSidebar({
3413
3458
  />
3414
3459
  </label>
3415
3460
  <label className="block space-y-1">
3416
- <div className="text-[11px]">画笔粗细</div>
3461
+ <div className="text-[11px]">{t("panel.config.penWidth")}</div>
3417
3462
  <Input
3418
3463
  type="number"
3419
3464
  min={1}
@@ -3481,7 +3526,7 @@ export function PanelConfigSidebar({
3481
3526
  updateSelectedGeometry({ geometrySketchDataUrl: canvas.toDataURL("image/png") });
3482
3527
  }}
3483
3528
  >
3484
- 应用手绘到节点
3529
+ {t("panel.config.applySketch")}
3485
3530
  </button>
3486
3531
  <button
3487
3532
  type="button"
@@ -3491,25 +3536,25 @@ export function PanelConfigSidebar({
3491
3536
  redrawGeometryPadFromElement();
3492
3537
  }}
3493
3538
  >
3494
- 清空手绘
3539
+ {t("panel.config.clearSketch")}
3495
3540
  </button>
3496
3541
  </div>
3497
3542
  </>
3498
3543
  )}
3499
3544
  </>,
3500
3545
  true,
3501
- ["几何", "geometry", "形状", "canvas", "脚本", "手绘"]
3546
+ [t("panel.material.geometry"), "geometry", t("panel.config.shape"), "canvas", t("panel.config.searchKwScript"), t("panel.config.searchKwSketch")]
3502
3547
  )
3503
3548
  ) : selectedElement.materialType === "grid" ? (
3504
3549
  renderSection(
3505
3550
  "gridConfig",
3506
- "网格布局配置",
3551
+ t("panel.config.sectionGrid"),
3507
3552
  <>
3508
3553
  {renderFieldGroup(
3509
- "网格参数",
3554
+ t("panel.config.groupGridParams"),
3510
3555
  <div className="grid grid-cols-2 gap-2">
3511
3556
  <label className="block space-y-1">
3512
- <div>行数</div>
3557
+ <div>{t("panel.config.rowCount")}</div>
3513
3558
  <Input
3514
3559
  type="number"
3515
3560
  min={1}
@@ -3524,7 +3569,7 @@ export function PanelConfigSidebar({
3524
3569
  />
3525
3570
  </label>
3526
3571
  <label className="block space-y-1">
3527
- <div>列数</div>
3572
+ <div>{t("panel.config.colCount")}</div>
3528
3573
  <Input
3529
3574
  type="number"
3530
3575
  min={1}
@@ -3539,7 +3584,7 @@ export function PanelConfigSidebar({
3539
3584
  />
3540
3585
  </label>
3541
3586
  <label className="block space-y-1">
3542
- <div>间距(px)</div>
3587
+ <div>{t("panel.config.gapPx")}</div>
3543
3588
  <Input
3544
3589
  type="number"
3545
3590
  min={0}
@@ -3554,7 +3599,7 @@ export function PanelConfigSidebar({
3554
3599
  />
3555
3600
  </label>
3556
3601
  <label className="block space-y-1">
3557
- <div>内边距(px)</div>
3602
+ <div>{t("panel.config.paddingPx")}</div>
3558
3603
  <Input
3559
3604
  type="number"
3560
3605
  min={0}
@@ -3569,7 +3614,7 @@ export function PanelConfigSidebar({
3569
3614
  />
3570
3615
  </label>
3571
3616
  <label className="block space-y-1">
3572
- <div>吸附阈值(px)</div>
3617
+ <div>{t("panel.config.snapThresholdPx")}</div>
3573
3618
  <Input
3574
3619
  type="number"
3575
3620
  min={8}
@@ -3590,20 +3635,20 @@ export function PanelConfigSidebar({
3590
3635
  )}
3591
3636
  </>,
3592
3637
  true,
3593
- ["网格", "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")],
3594
3639
  <>
3595
- 其他节点拖拽靠近该网格槽位中心时会自动吸附,并在节点树显示为该网格子节点。
3640
+ {t("panel.config.gridHint")}
3596
3641
  </>
3597
3642
  )
3598
3643
  ) : selectedElement.materialType === "reference" ? (
3599
3644
  renderSection(
3600
3645
  "reference",
3601
- "引用组件配置",
3646
+ t("panel.config.sectionReference"),
3602
3647
  <>
3603
3648
  {renderFieldGroup(
3604
- "引用源",
3649
+ t("panel.config.groupRefSource"),
3605
3650
  <label className="block space-y-1">
3606
- <div>引用图层</div>
3651
+ <div>{t("panel.config.refLayer")}</div>
3607
3652
  <Select
3608
3653
  value={selectedElement.refLayerId ?? "__none__"}
3609
3654
  onValueChange={(value) =>
@@ -3613,10 +3658,10 @@ export function PanelConfigSidebar({
3613
3658
  }
3614
3659
  >
3615
3660
  <SelectTrigger className="h-7">
3616
- <SelectValue placeholder="请选择图层" />
3661
+ <SelectValue placeholder={t("panel.config.pleaseSelectLayer")} />
3617
3662
  </SelectTrigger>
3618
3663
  <SelectContent>
3619
- <SelectItem value="__none__">无(不引用)</SelectItem>
3664
+ <SelectItem value="__none__">{t("panel.config.noneNoRef")}</SelectItem>
3620
3665
  {layers
3621
3666
  .filter((l) => l.id !== selectedElement.layerId)
3622
3667
  .map((l) => (
@@ -3629,10 +3674,10 @@ export function PanelConfigSidebar({
3629
3674
  </label>
3630
3675
  )}
3631
3676
  {renderFieldGroup(
3632
- "拷贝策略",
3677
+ t("panel.config.groupCopyStrategy"),
3633
3678
  <>
3634
3679
  <label className="block space-y-1">
3635
- <div>拷贝模式</div>
3680
+ <div>{t("panel.config.copyMode")}</div>
3636
3681
  <Select
3637
3682
  value={selectedElement.refCopyMode ?? "shallow"}
3638
3683
  onValueChange={(value) =>
@@ -3643,31 +3688,31 @@ export function PanelConfigSidebar({
3643
3688
  }
3644
3689
  >
3645
3690
  <SelectTrigger className="h-7">
3646
- <SelectValue placeholder="请选择拷贝模式" />
3691
+ <SelectValue placeholder={t("panel.config.selectCopyMode")} />
3647
3692
  </SelectTrigger>
3648
3693
  <SelectContent>
3649
- <SelectItem value="shallow">浅拷贝(跟随源图层变化)</SelectItem>
3650
- <SelectItem value="deep">深拷贝(冻结当前引用快照)</SelectItem>
3694
+ <SelectItem value="shallow">{t("panel.config.shallowFollow")}</SelectItem>
3695
+ <SelectItem value="deep">{t("panel.config.deepFreeze")}</SelectItem>
3651
3696
  </SelectContent>
3652
3697
  </Select>
3653
3698
  </label>
3654
3699
  </>,
3655
3700
  <>
3656
- 浅拷贝会实时同步被引用图层;深拷贝会固定当前快照,不再随源变化。
3701
+ {t("panel.config.copyStrategyHint")}
3657
3702
  </>
3658
3703
  )}
3659
3704
  </>,
3660
3705
  true,
3661
- ["引用", "ref", "图层", "浅拷贝", "深拷贝", "snapshot"]
3706
+ [t("panel.config.searchKwRef"), "ref", t("panel.config.layer"), t("panel.material.shallowCopy"), t("panel.material.deepCopy"), "snapshot"]
3662
3707
  )
3663
3708
  ) : (
3664
3709
  <div className="text-xs leading-6 text-muted-foreground">
3665
- 当前节点不是图表类型,暂无图表配置项。
3710
+ {t("panel.config.notChartType")}
3666
3711
  </div>
3667
3712
  )}
3668
3713
  {hasSearch && renderedSectionCount === 0 ? (
3669
3714
  <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
3670
- 未找到匹配项,请尝试更换关键词。
3715
+ {t("panel.config.noMatch")}
3671
3716
  </div>
3672
3717
  ) : null}
3673
3718
  </div>