@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, { useMemo, useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
  import {
3
4
  Card,
4
5
  CardContent,
@@ -23,7 +24,7 @@ import {
23
24
  } from "@arronqzy/ui";
24
25
  import type { PanelElement, ReferenceCopyMode } from "../types";
25
26
  import type { PanelLayer } from "../types";
26
- import { PANEL_MESSAGES } from "../constants/messages";
27
+ import { getPanelMessages } from "../constants/messages";
27
28
  import {
28
29
  concreteGridParentIdForLayer,
29
30
  logicalGridParentIdFromConcrete,
@@ -50,25 +51,34 @@ function compareGridTreeChildOrder(a: PanelElement, b: PanelElement): number {
50
51
  return a.id.localeCompare(b.id);
51
52
  }
52
53
 
53
- const MATERIAL_LABEL_MAP: Record<string, string> = {
54
- bar: "柱状图",
55
- line: "折线图",
56
- pie: "饼图",
57
- area: "面积图",
58
- scatter: "散点图",
59
- radar: "雷达图",
60
- gauge: "仪表盘",
61
- funnel: "漏斗图",
62
- text: "文本",
63
- rect: "矩形",
64
- grid: "网格布局",
65
- image: "图片",
66
- video: "视频",
67
- audio: "音频",
68
- reference: "引用组件",
69
- geometry: "几何",
54
+ const MATERIAL_LABEL_KEYS: Record<string, string> = {
55
+ bar: "panel.material.bar",
56
+ line: "panel.material.line",
57
+ pie: "panel.material.pie",
58
+ area: "panel.material.area",
59
+ scatter: "panel.material.scatter",
60
+ radar: "panel.material.radar",
61
+ gauge: "panel.material.gauge",
62
+ funnel: "panel.material.funnel",
63
+ text: "panel.material.text",
64
+ rect: "panel.material.rect",
65
+ grid: "panel.material.grid",
66
+ image: "panel.material.image",
67
+ video: "panel.material.video",
68
+ audio: "panel.material.audio",
69
+ reference: "panel.material.reference",
70
+ geometry: "panel.material.geometry",
71
+ table: "panel.material.table",
70
72
  };
71
73
 
74
+ function getMaterialLabelMap(t: (key: string) => string): Record<string, string> {
75
+ const map: Record<string, string> = {};
76
+ for (const [id, key] of Object.entries(MATERIAL_LABEL_KEYS)) {
77
+ map[id] = t(key);
78
+ }
79
+ return map;
80
+ }
81
+
72
82
  function LockGlyph({ className = "h-3.5 w-3.5" }: { className?: string }) {
73
83
  return (
74
84
  <svg
@@ -129,6 +139,7 @@ function IconDeepCopy({ className = "h-3.5 w-3.5" }: { className?: string }) {
129
139
  }
130
140
 
131
141
  function MaterialPreview({ id }: { id: string }) {
142
+ const { t } = useI18n();
132
143
  const common =
133
144
  "relative aspect-[4/3] w-full shrink-0 overflow-hidden rounded-md border border-border bg-gradient-to-br from-card to-muted/40";
134
145
 
@@ -243,6 +254,28 @@ function MaterialPreview({ id }: { id: string }) {
243
254
  );
244
255
  }
245
256
 
257
+ if (id === "table") {
258
+ return (
259
+ <div className={common}>
260
+ <div className="absolute inset-0 flex flex-col gap-1 p-2">
261
+ <div className="grid h-3 flex-none grid-cols-3 gap-1">
262
+ <div className="rounded-sm bg-primary/75" />
263
+ <div className="rounded-sm bg-primary/65" />
264
+ <div className="rounded-sm bg-primary/70" />
265
+ </div>
266
+ <div className="grid min-h-0 flex-1 grid-cols-3 gap-1">
267
+ {Array.from({ length: 6 }).map((_, idx) => (
268
+ <div
269
+ key={idx}
270
+ className="rounded-sm border border-primary/25 bg-primary/10"
271
+ />
272
+ ))}
273
+ </div>
274
+ </div>
275
+ </div>
276
+ );
277
+ }
278
+
246
279
  if (id === "rect") {
247
280
  return (
248
281
  <div className={common}>
@@ -270,7 +303,7 @@ function MaterialPreview({ id }: { id: string }) {
270
303
  <div className={common}>
271
304
  <div className="absolute inset-2 rounded-md border border-dashed border-primary/70 bg-primary/10" />
272
305
  <div className="absolute inset-0 flex items-center justify-center text-[10px] text-primary/80">
273
- 引用组件
306
+ {t("panel.material.reference")}
274
307
  </div>
275
308
  </div>
276
309
  );
@@ -331,41 +364,44 @@ function MaterialPreview({ id }: { id: string }) {
331
364
  return <div className={common} />;
332
365
  }
333
366
 
334
- const defaultCategories: MaterialCategory[] = [
335
- {
336
- id: "charts",
337
- title: "图表",
338
- items: [
339
- { id: "bar", title: "柱状图" },
340
- { id: "line", title: "折线图" },
341
- { id: "pie", title: "饼图" },
342
- { id: "area", title: "面积图" },
343
- { id: "scatter", title: "散点图" },
344
- { id: "radar", title: "雷达图" },
345
- { id: "gauge", title: "仪表盘" },
346
- { id: "funnel", title: "漏斗图" },
347
- ],
348
- },
349
- {
350
- id: "basic",
351
- title: "基础",
352
- items: [
353
- { id: "text", title: "文本" },
354
- { id: "geometry", title: "几何" },
355
- { id: "grid", title: "网格布局" },
356
- { id: "image", title: "图片" },
357
- { id: "reference", title: "引用组件" },
358
- ],
359
- },
360
- {
361
- id: "media",
362
- title: "媒体",
363
- items: [
364
- { id: "video", title: "视频" },
365
- { id: "audio", title: "音频" },
366
- ],
367
- },
368
- ];
367
+ function getDefaultCategories(t: (key: string) => string): MaterialCategory[] {
368
+ return [
369
+ {
370
+ id: "charts",
371
+ title: t("panel.material.charts"),
372
+ items: [
373
+ { id: "bar", title: t("panel.material.bar") },
374
+ { id: "line", title: t("panel.material.line") },
375
+ { id: "pie", title: t("panel.material.pie") },
376
+ { id: "area", title: t("panel.material.area") },
377
+ { id: "scatter", title: t("panel.material.scatter") },
378
+ { id: "radar", title: t("panel.material.radar") },
379
+ { id: "gauge", title: t("panel.material.gauge") },
380
+ { id: "funnel", title: t("panel.material.funnel") },
381
+ ],
382
+ },
383
+ {
384
+ id: "basic",
385
+ title: t("panel.material.basic"),
386
+ items: [
387
+ { id: "text", title: t("panel.material.text") },
388
+ { id: "table", title: t("panel.material.table") },
389
+ { id: "geometry", title: t("panel.material.geometry") },
390
+ { id: "grid", title: t("panel.material.grid") },
391
+ { id: "image", title: t("panel.material.image") },
392
+ { id: "reference", title: t("panel.material.reference") },
393
+ ],
394
+ },
395
+ {
396
+ id: "media",
397
+ title: t("panel.material.media"),
398
+ items: [
399
+ { id: "video", title: t("panel.material.video") },
400
+ { id: "audio", title: t("panel.material.audio") },
401
+ ],
402
+ },
403
+ ];
404
+ }
369
405
 
370
406
  export type MaterialSidebarProps = {
371
407
  className?: string;
@@ -392,7 +428,11 @@ export function MaterialSidebar({
392
428
  onCopyNode,
393
429
  onMoveNodeToLayer,
394
430
  }: MaterialSidebarProps) {
395
- const categories = useMemo(() => defaultCategories, []);
431
+ const { t } = useI18n();
432
+ const messages = useMemo(() => getPanelMessages(t), [t]);
433
+ const materialLabels = useMemo(() => getMaterialLabelMap(t), [t]);
434
+
435
+ const categories = useMemo(() => getDefaultCategories(t), [t]);
396
436
  const [leftTab, setLeftTab] = useState<"materials" | "tree">("materials");
397
437
  const [activeCategoryId, setActiveCategoryId] =
398
438
  useState<MaterialCategoryId>("charts");
@@ -502,7 +542,7 @@ export function MaterialSidebar({
502
542
 
503
543
  const getNodeDisplayName = (node: PanelElement) => {
504
544
  const customName = node.name?.trim();
505
- return customName || node.chart?.title || MATERIAL_LABEL_MAP[node.materialType ?? ""] || node.id;
545
+ return customName || node.chart?.title || materialLabels[node.materialType ?? ""] || node.id;
506
546
  };
507
547
 
508
548
  const isExpanded = (key: string, defaultValue = false) =>
@@ -631,32 +671,36 @@ export function MaterialSidebar({
631
671
  className="inline-flex shrink-0 items-center rounded-md border-2 border-violet-600 bg-violet-500/20 px-1.5 py-0.5 text-[10px] font-semibold text-violet-950 shadow-sm dark:border-violet-400 dark:bg-violet-500/35 dark:text-violet-50 dark:shadow-[0_0_14px_-3px_rgba(167,139,250,0.65)]"
632
672
  title={
633
673
  nodeHomeLayer.mappingBaseLayerId
634
- ? `映射图层「${nodeHomeLayer.name}」· 基准图层:${
635
- layerById.get(nodeHomeLayer.mappingBaseLayerId)?.name ??
636
- nodeHomeLayer.mappingBaseLayerId
637
- }`
638
- : `映射图层「${nodeHomeLayer.name}」`
674
+ ? t("panel.material.mappingLayerWithBase", {
675
+ name: nodeHomeLayer.name,
676
+ base:
677
+ layerById.get(nodeHomeLayer.mappingBaseLayerId)?.name ??
678
+ nodeHomeLayer.mappingBaseLayerId,
679
+ })
680
+ : t("panel.material.mappingLayerTitle", { name: nodeHomeLayer.name })
639
681
  }
640
682
  >
641
- 映射图层节点
683
+ {t("panel.material.mappingLayerNode")}
642
684
  </span>
643
685
  ) : null}
644
686
  {node.mappingSourceNodeId ? (
645
687
  <span
646
688
  className="inline-flex shrink-0 items-center rounded border border-primary/40 bg-primary/10 px-1 text-[10px] text-primary"
647
- title={`同源节点 ${node.mappingSourceNodeId}${
648
- node.mappingSourceLayerId
649
- ? ` · ${layerById.get(node.mappingSourceLayerId)?.name ?? node.mappingSourceLayerId}`
650
- : ""
651
- }`}
689
+ title={t("panel.material.sameSourceTitle", {
690
+ id: `${node.mappingSourceNodeId}${
691
+ node.mappingSourceLayerId
692
+ ? ` · ${layerById.get(node.mappingSourceLayerId)?.name ?? node.mappingSourceLayerId}`
693
+ : ""
694
+ }`,
695
+ })}
652
696
  >
653
- 同源
697
+ {t("panel.material.sameSource")}
654
698
  </span>
655
699
  ) : null}
656
700
  {node.locked ? (
657
701
  <span
658
702
  className="inline-flex h-5 w-5 items-center justify-center rounded border border-border/80 bg-background/80 text-muted-foreground"
659
- title="节点已锁定"
703
+ title={t("panel.material.nodeLocked")}
660
704
  >
661
705
  <LockGlyph />
662
706
  </span>
@@ -670,9 +714,9 @@ export function MaterialSidebar({
670
714
  ? "border-violet-500/50 bg-violet-500/15 text-violet-300"
671
715
  : "border-sky-500/40 bg-sky-500/10 text-sky-300",
672
716
  ].join(" ")}
673
- title={isDeepRef ? "深拷贝引用(冻结快照)" : "浅拷贝引用(跟随源变化)"}
717
+ title={isDeepRef ? t("panel.material.deepCopyTitle") : t("panel.material.shallowCopyTitle")}
674
718
  >
675
- {isDeepRef ? "深拷贝" : "浅拷贝"}
719
+ {isDeepRef ? t("panel.material.deepCopy") : t("panel.material.shallowCopy")}
676
720
  </span>
677
721
  <Tooltip>
678
722
  <TooltipTrigger asChild>
@@ -683,12 +727,12 @@ export function MaterialSidebar({
683
727
  e.stopPropagation();
684
728
  onCopyNode?.(node.id, "shallow");
685
729
  }}
686
- aria-label="浅拷贝"
730
+ aria-label={t("panel.material.shallowCopy")}
687
731
  >
688
732
  <IconShallowCopy />
689
733
  </button>
690
734
  </TooltipTrigger>
691
- <TooltipContent>浅拷贝</TooltipContent>
735
+ <TooltipContent>{t("panel.material.shallowCopy")}</TooltipContent>
692
736
  </Tooltip>
693
737
  <Tooltip>
694
738
  <TooltipTrigger asChild>
@@ -699,12 +743,12 @@ export function MaterialSidebar({
699
743
  e.stopPropagation();
700
744
  onCopyNode?.(node.id, "deep");
701
745
  }}
702
- aria-label="深拷贝"
746
+ aria-label={t("panel.material.deepCopy")}
703
747
  >
704
748
  <IconDeepCopy />
705
749
  </button>
706
750
  </TooltipTrigger>
707
- <TooltipContent>深拷贝</TooltipContent>
751
+ <TooltipContent>{t("panel.material.deepCopy")}</TooltipContent>
708
752
  </Tooltip>
709
753
  </>
710
754
  ) : (
@@ -717,12 +761,12 @@ export function MaterialSidebar({
717
761
  e.stopPropagation();
718
762
  onCopyNode?.(node.id);
719
763
  }}
720
- aria-label="复制节点"
764
+ aria-label={t("panel.material.duplicateNode")}
721
765
  >
722
766
  <IconCopy />
723
767
  </button>
724
768
  </TooltipTrigger>
725
- <TooltipContent>复制节点</TooltipContent>
769
+ <TooltipContent>{t("panel.material.duplicateNode")}</TooltipContent>
726
770
  </Tooltip>
727
771
  )}
728
772
  <Tooltip>
@@ -736,12 +780,12 @@ export function MaterialSidebar({
736
780
  if (node.locked) return;
737
781
  onDeleteNode?.(node.id);
738
782
  }}
739
- aria-label={node.locked ? "锁定节点不可删除" : "删除节点"}
783
+ aria-label={node.locked ? t("panel.material.lockedNodeCannotDelete") : t("panel.material.deleteNode")}
740
784
  >
741
785
  <IconDelete />
742
786
  </button>
743
787
  </TooltipTrigger>
744
- <TooltipContent>{node.locked ? "锁定节点不可删除" : "删除节点"}</TooltipContent>
788
+ <TooltipContent>{node.locked ? t("panel.material.lockedNodeCannotDelete") : t("panel.material.deleteNode")}</TooltipContent>
745
789
  </Tooltip>
746
790
  </div>
747
791
  {hasChildren ? (
@@ -754,9 +798,10 @@ export function MaterialSidebar({
754
798
  key={`cycle:${childPath}`}
755
799
  className="py-1 text-[10px] text-muted-foreground/80"
756
800
  style={{ paddingLeft: 6 + (level + 1) * 14 }}
757
- title="检测到循环引用,已停止向下展开"
801
+ title={t("panel.material.circularRefStopped")}
758
802
  >
759
- {getNodeDisplayName(child)}(循环引用)
803
+ {getNodeDisplayName(child)}
804
+ {t("panel.material.circularRefSuffix")}
760
805
  </div>
761
806
  );
762
807
  }
@@ -796,7 +841,7 @@ export function MaterialSidebar({
796
841
  <Input
797
842
  value={keyword}
798
843
  onChange={(e) => setKeyword(e.target.value)}
799
- placeholder="搜索物料(名称 / 分类)"
844
+ placeholder={t("panel.material.searchPlaceholder")}
800
845
  className="h-8 text-xs"
801
846
  />
802
847
  </div>
@@ -808,8 +853,8 @@ export function MaterialSidebar({
808
853
  >
809
854
  <div className="border-b border-border px-2 pb-2 pt-2">
810
855
  <TabsList className="grid w-full grid-cols-2">
811
- <TabsTrigger value="materials">物料</TabsTrigger>
812
- <TabsTrigger value="tree">节点树</TabsTrigger>
856
+ <TabsTrigger value="materials">{t("panel.material.tabMaterials")}</TabsTrigger>
857
+ <TabsTrigger value="tree">{t("panel.material.tabTree")}</TabsTrigger>
813
858
  </TabsList>
814
859
  </div>
815
860
  <TabsContent value="materials" className="mt-0 min-h-0 flex-1 overflow-hidden">
@@ -837,7 +882,9 @@ export function MaterialSidebar({
837
882
 
838
883
  <div className={`overflow-auto ${themedScrollbarClass}`}>
839
884
  <div className="px-2.5 py-2.5 text-xs font-semibold">
840
- {isSearching ? `搜索结果(${matchedItems.length})` : activeCategory.title}
885
+ {isSearching
886
+ ? t("panel.material.searchResults", { count: matchedItems.length })
887
+ : activeCategory.title}
841
888
  </div>
842
889
  <div className="grid gap-2 px-2.5 pb-3">
843
890
  {(isSearching ? matchedItems : activeCategory.items).map((it) => (
@@ -883,9 +930,9 @@ export function MaterialSidebar({
883
930
  <path d="m20 20-3.5-3.5" />
884
931
  </svg>
885
932
  </EmptyIcon>
886
- <EmptyTitle className="text-xs">未匹配到物料</EmptyTitle>
933
+ <EmptyTitle className="text-xs">{t("panel.material.emptyMaterialsTitle")}</EmptyTitle>
887
934
  <EmptyDescription className="text-[11px]">
888
- 尝试更换关键词,或切换分类后再搜索。
935
+ {t("panel.material.emptyMaterialsDesc")}
889
936
  </EmptyDescription>
890
937
  </Empty>
891
938
  ) : null}
@@ -899,16 +946,16 @@ export function MaterialSidebar({
899
946
  <Input
900
947
  value={treeKeyword}
901
948
  onChange={(e) => setTreeKeyword(e.target.value)}
902
- placeholder="搜索节点(名称 / 类型 / ID)"
949
+ placeholder={t("panel.material.treeSearchPlaceholder")}
903
950
  className="h-8 text-xs"
904
951
  />
905
952
  </div>
906
953
  <div className="mb-2 flex items-center justify-between rounded border border-border bg-card px-2 py-1.5">
907
- <span className="text-[11px] text-muted-foreground">仅看引用子树</span>
954
+ <span className="text-[11px] text-muted-foreground">{t("panel.material.referenceOnlyTree")}</span>
908
955
  <Switch
909
956
  checked={referenceOnlyTree}
910
957
  onCheckedChange={setReferenceOnlyTree}
911
- aria-label="仅看引用子树"
958
+ aria-label={t("panel.material.referenceOnlyTree")}
912
959
  />
913
960
  </div>
914
961
  <Collapsible
@@ -937,13 +984,13 @@ export function MaterialSidebar({
937
984
  const dropBlockReason = !draggingNode
938
985
  ? ""
939
986
  : draggingNode.locked
940
- ? PANEL_MESSAGES.nodeMoveLocked
987
+ ? messages.nodeMoveLocked
941
988
  : draggingSourceLayer?.locked
942
- ? PANEL_MESSAGES.nodeMoveSourceLayerLocked
989
+ ? messages.nodeMoveSourceLayerLocked
943
990
  : layer.locked
944
- ? PANEL_MESSAGES.nodeMoveTargetLayerLocked
991
+ ? messages.nodeMoveTargetLayerLocked
945
992
  : draggingNode.layerId === layer.id
946
- ? PANEL_MESSAGES.nodeMoveSameLayer
993
+ ? messages.nodeMoveSameLayer
947
994
  : "";
948
995
  const canDropIntoLayer = Boolean(draggingNode) && !dropBlockReason;
949
996
  const isCurrentDropLayer = dragOverLayerId === layer.id;
@@ -983,7 +1030,7 @@ export function MaterialSidebar({
983
1030
  ? "bg-primary/15 text-primary"
984
1031
  : "bg-destructive/15 text-destructive",
985
1032
  ].join(" ")}
986
- title={canDropIntoLayer ? "目标图层" : dropBlockReason}
1033
+ title={canDropIntoLayer ? t("panel.material.targetLayer") : dropBlockReason}
987
1034
  >
988
1035
  ➜
989
1036
  </span>
@@ -1007,14 +1054,15 @@ export function MaterialSidebar({
1007
1054
  className="shrink-0 rounded-md border-2 border-violet-600 bg-violet-500/22 px-1.5 py-0.5 text-[10px] font-semibold text-violet-950 shadow-sm dark:border-violet-400 dark:bg-violet-500/35 dark:text-violet-50 dark:shadow-[0_0_12px_-2px_rgba(167,139,250,0.55)]"
1008
1055
  title={
1009
1056
  layer.mappingBaseLayerId
1010
- ? `映射图层 · 基准图层:${
1011
- layerById.get(layer.mappingBaseLayerId)?.name ??
1012
- layer.mappingBaseLayerId
1013
- }`
1014
- : "映射图层"
1057
+ ? t("panel.material.mappingLayerWithBaseShort", {
1058
+ base:
1059
+ layerById.get(layer.mappingBaseLayerId)?.name ??
1060
+ layer.mappingBaseLayerId,
1061
+ })
1062
+ : t("panel.material.mappingLayer")
1015
1063
  }
1016
1064
  >
1017
- 映射图层
1065
+ {t("panel.material.mappingLayer")}
1018
1066
  </span>
1019
1067
  ) : null}
1020
1068
  {isCurrentDropLayer ? (
@@ -1026,7 +1074,7 @@ export function MaterialSidebar({
1026
1074
  : "bg-destructive/15 text-destructive",
1027
1075
  ].join(" ")}
1028
1076
  >
1029
- {canDropIntoLayer ? "将移动到该图层" : dropBlockReason}
1077
+ {canDropIntoLayer ? t("panel.material.willMoveToLayer") : dropBlockReason}
1030
1078
  </span>
1031
1079
  ) : null}
1032
1080
  </CardHeader>
@@ -1075,18 +1123,18 @@ export function MaterialSidebar({
1075
1123
  setDragOverLayerId(null);
1076
1124
  }
1077
1125
  }}
1078
- title="拖拽节点到此图层"
1126
+ title={t("panel.material.dragToThisLayer")}
1079
1127
  >
1080
1128
  {draggingTreeNodeId
1081
1129
  ? canDropIntoLayer
1082
- ? "释放以移动到该图层"
1083
- : dropBlockReason || "不可移动到该图层"
1084
- : "拖拽节点到该图层"}
1130
+ ? t("panel.material.releaseToMove")
1131
+ : dropBlockReason || t("panel.material.cannotMoveToLayer")
1132
+ : t("panel.material.dragToLayer")}
1085
1133
  </div>
1086
1134
  <CollapsibleContent>
1087
1135
  {rootNodes.length === 0 ? (
1088
1136
  <div className="rounded border border-border/40 bg-muted/15 py-2 pl-3 text-[11px] text-muted-foreground">
1089
- 空图层
1137
+ {t("panel.material.emptyLayer")}
1090
1138
  </div>
1091
1139
  ) : (
1092
1140
  rootNodes.map((node) =>
@@ -1123,9 +1171,9 @@ export function MaterialSidebar({
1123
1171
  <path d="M8 10h8M8 14h5" />
1124
1172
  </svg>
1125
1173
  </EmptyIcon>
1126
- <EmptyTitle className="text-xs">未匹配到节点</EmptyTitle>
1174
+ <EmptyTitle className="text-xs">{t("panel.material.emptyNodesTitle")}</EmptyTitle>
1127
1175
  <EmptyDescription className="text-[11px]">
1128
- 试试节点名称、类型或 ID 关键词。
1176
+ {t("panel.material.emptyNodesDesc")}
1129
1177
  </EmptyDescription>
1130
1178
  </Empty>
1131
1179
  </div>
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
  import { createPortal } from "react-dom";
3
4
  import Moveable from "react-moveable";
4
5
 
@@ -177,6 +178,8 @@ export function MoveableLayer({
177
178
  refreshToken,
178
179
  viewportSyncRef,
179
180
  }: MoveableLayerProps) {
181
+ const { t } = useI18n();
182
+
180
183
  const moveableRef = useRef<any>(null);
181
184
  const targets = useMemo(() => {
182
185
  const validTargets = selectedTargets.filter(
@@ -453,10 +456,9 @@ export function MoveableLayer({
453
456
  <div
454
457
  className="pointer-events-none absolute z-[80] inline-flex select-none items-center gap-1 rounded border border-border bg-background/95 px-1.5 py-0.5 text-[11px] text-foreground shadow-sm"
455
458
  style={{ left: lockBadgeScreen.x + 6, top: lockBadgeScreen.y - 22 }}
456
- title="节点已锁定"
457
- >
459
+ title={t("panel.config.nodeLockedTitle")}>
458
460
  <LockGlyph />
459
- <span>已锁定</span>
461
+ <span>{t("panel.config.nodeLockedBadge")}</span>
460
462
  </div>
461
463
  ) : null;
462
464
 
@@ -802,10 +804,9 @@ export function MoveableLayer({
802
804
  <div
803
805
  className="pointer-events-none absolute z-[80] inline-flex select-none items-center gap-1 rounded border border-border bg-background/95 px-1.5 py-0.5 text-[11px] text-foreground shadow-sm"
804
806
  style={{ left: lockBadgeScreen.x + 6, top: lockBadgeScreen.y - 22 }}
805
- title="部分节点已锁定,已自动排除"
806
- >
807
+ title={t("panel.config.lockedExcludedTitle")}>
807
808
  <LockGlyph />
808
- <span>锁定节点已排除</span>
809
+ <span>{t("panel.config.lockedExcludedBadge")}</span>
809
810
  </div>
810
811
  ) : null}
811
812
  </>