@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
@@ -0,0 +1,137 @@
1
+ /** Map panel.update patch keys → config field anchors / sidebar sections. */
2
+
3
+ export type ConfigRevealTarget = {
4
+ field: string;
5
+ section: string;
6
+ };
7
+
8
+ const FIELD_SECTION: Record<string, string> = {
9
+ name: "nodeInfo",
10
+ x: "nodeInfo",
11
+ y: "nodeInfo",
12
+ width: "nodeInfo",
13
+ height: "nodeInfo",
14
+ rotate: "nodeInfo",
15
+ locked: "nodeInfo",
16
+ zIndex: "nodeInfo",
17
+ layerId: "nodeInfo",
18
+ textHtml: "textConfig",
19
+ textColor: "textConfig",
20
+ textFontSize: "textConfig",
21
+ textFontFamily: "textConfig",
22
+ textFontWeight: "textConfig",
23
+ textAlign: "textConfig",
24
+ textLineHeight: "textConfig",
25
+ table: "tableConfig",
26
+ "table.source": "tableConfig",
27
+ "table.rowsText": "tableConfig",
28
+ "table.columns": "tableConfig",
29
+ "table.emptyText": "tableConfig",
30
+ "table.showHeader": "tableConfig",
31
+ "table.stripe": "tableConfig",
32
+ "table.rowHeight": "tableConfig",
33
+ chart: "chartBasic",
34
+ "chart.title": "chartBasic",
35
+ "chart.labelsText": "chartBasic",
36
+ "chart.valuesText": "chartBasic",
37
+ "chart.color": "chartBasic",
38
+ style: "nodeInfo",
39
+ "style.backgroundColor": "nodeInfo",
40
+ "style.borderColor": "nodeInfo",
41
+ geometryShape: "geometryConfig",
42
+ geometryColor: "geometryConfig",
43
+ };
44
+
45
+ export function configTargetsFromPatch(
46
+ patch: Record<string, unknown>
47
+ ): ConfigRevealTarget[] {
48
+ const out: ConfigRevealTarget[] = [];
49
+ const seen = new Set<string>();
50
+
51
+ const push = (field: string) => {
52
+ const section = FIELD_SECTION[field] ?? FIELD_SECTION[field.split(".")[0]!] ?? "nodeInfo";
53
+ if (seen.has(field)) return;
54
+ seen.add(field);
55
+ out.push({ field, section });
56
+ };
57
+
58
+ for (const key of Object.keys(patch)) {
59
+ if (key === "table" && patch.table && typeof patch.table === "object") {
60
+ for (const sub of Object.keys(patch.table as object)) {
61
+ push(`table.${sub}`);
62
+ }
63
+ continue;
64
+ }
65
+ if (key === "chart" && patch.chart && typeof patch.chart === "object") {
66
+ for (const sub of Object.keys(patch.chart as object)) {
67
+ push(`chart.${sub}`);
68
+ }
69
+ continue;
70
+ }
71
+ if (key === "style" && patch.style && typeof patch.style === "object") {
72
+ for (const sub of Object.keys(patch.style as object)) {
73
+ push(`style.${sub}`);
74
+ }
75
+ continue;
76
+ }
77
+ push(key);
78
+ }
79
+
80
+ return out;
81
+ }
82
+
83
+ const REVEAL_EVENT = "abuilder:reveal-config";
84
+
85
+ export type RevealConfigDetail = {
86
+ fields: string[];
87
+ sections: string[];
88
+ };
89
+
90
+ export function revealPanelConfig(targets: ConfigRevealTarget[]) {
91
+ if (typeof window === "undefined" || !targets.length) return;
92
+ const detail: RevealConfigDetail = {
93
+ fields: targets.map((t) => t.field),
94
+ sections: [...new Set(targets.map((t) => t.section))],
95
+ };
96
+ window.dispatchEvent(new CustomEvent(REVEAL_EVENT, { detail }));
97
+ }
98
+
99
+ export function revealPanelConfigFromPatch(patch: Record<string, unknown>) {
100
+ revealPanelConfig(configTargetsFromPatch(patch));
101
+ }
102
+
103
+ export function subscribeRevealPanelConfig(
104
+ handler: (detail: RevealConfigDetail) => void
105
+ ): () => void {
106
+ const listener = (event: Event) => {
107
+ const detail = (event as CustomEvent<RevealConfigDetail>).detail;
108
+ if (!detail) return;
109
+ handler(detail);
110
+ };
111
+ window.addEventListener(REVEAL_EVENT, listener);
112
+ return () => window.removeEventListener(REVEAL_EVENT, listener);
113
+ }
114
+
115
+ export function highlightConfigField(field: string) {
116
+ const el = document.querySelector<HTMLElement>(
117
+ `[data-config-field="${CSS.escape(field)}"]`
118
+ );
119
+ if (!el) return;
120
+
121
+ const scrollRoot = el.closest(".scope-config-sidebar") as HTMLElement | null;
122
+ if (scrollRoot) {
123
+ const rootRect = scrollRoot.getBoundingClientRect();
124
+ const elRect = el.getBoundingClientRect();
125
+ const top =
126
+ elRect.top -
127
+ rootRect.top +
128
+ scrollRoot.scrollTop -
129
+ scrollRoot.clientHeight / 3;
130
+ scrollRoot.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
131
+ } else {
132
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
133
+ }
134
+
135
+ el.classList.add("config-field--highlight");
136
+ window.setTimeout(() => el.classList.remove("config-field--highlight"), 1800);
137
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
  import {
3
4
  Tooltip,
4
5
  TooltipContent,
@@ -14,11 +15,14 @@ export type ConfigHintIconProps = {
14
15
  };
15
16
 
16
17
  export function ConfigHintIcon({
17
- label = "说明",
18
+ label,
18
19
  children,
19
20
  className,
20
21
  contentClassName,
21
22
  }: ConfigHintIconProps) {
23
+ const { t } = useI18n();
24
+ const resolvedLabel = label ?? t("common.hint");
25
+
22
26
  return (
23
27
  <TooltipProvider delayDuration={120}>
24
28
  <Tooltip>
@@ -29,7 +33,7 @@ export function ConfigHintIcon({
29
33
  className ??
30
34
  "inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] leading-none text-muted-foreground hover:bg-accent/50"
31
35
  }
32
- aria-label={`${label}说明`}
36
+ aria-label={t("common.hintAria", { label: resolvedLabel })}
33
37
  >
34
38
  ?
35
39
  </button>
@@ -1,8 +1,10 @@
1
1
  import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
  import * as echarts from "echarts";
3
4
  import type { PanelElement } from "../types";
4
5
  import { buildChartOption, CHART_TYPES } from "../utils/chartOptionBuilder";
5
6
  import { PREVIEW_LAYOUT_EVENT } from "../utils/panelStateIO";
7
+ import { TableNodeContent, type TableCellActionHandler } from "./table/TableNodeContent";
6
8
 
7
9
  export type ElementsLayerProps = {
8
10
  elements: PanelElement[];
@@ -19,6 +21,7 @@ export type ElementsLayerProps = {
19
21
  previewMode?: boolean;
20
22
  /** 预览布局变更时递增,用于触发图表/画布重绘 */
21
23
  previewLayoutKey?: number;
24
+ onTableCellAction?: TableCellActionHandler;
22
25
  };
23
26
 
24
27
  function TextNodeContent({
@@ -31,7 +34,8 @@ function TextNodeContent({
31
34
  onChange: (nextHtml: string) => void;
32
35
  }) {
33
36
  const ref = useRef<HTMLDivElement | null>(null);
34
- const html = element.textHtml ?? "<p>双击输入文本</p>";
37
+ const { t } = useI18n();
38
+ const html = element.textHtml ?? `<p>${t("panel.defaults.doubleClickTextHtml")}</p>`;
35
39
  const textStyle: React.CSSProperties = {
36
40
  fontFamily: element.textFontFamily || undefined,
37
41
  fontSize: element.textFontSize ? `${element.textFontSize}px` : undefined,
@@ -71,6 +75,7 @@ function GridNodeContent({
71
75
  allElements: PanelElement[];
72
76
  previewMode?: boolean;
73
77
  }) {
78
+ const { t } = useI18n();
74
79
  const rows = Math.max(1, Math.floor(element.gridRows ?? 2));
75
80
  const cols = Math.max(1, Math.floor(element.gridCols ?? 3));
76
81
  const gap = Math.max(0, element.gridGap ?? 8);
@@ -142,7 +147,7 @@ function GridNodeContent({
142
147
  ? "border-primary/70 bg-primary/10"
143
148
  : "border-border/60 bg-muted/20",
144
149
  ].join(" ")}
145
- title={`槽位 ${idx + 1}${occupied.has(idx) ? "(已占用)" : "(空)"}`}
150
+ title={occupied.has(idx) ? t("panel.config.slotOccupied", { n: idx + 1 }) : t("panel.config.slotEmpty", { n: idx + 1 })}
146
151
  />
147
152
  ))}
148
153
  </div>
@@ -179,6 +184,7 @@ function AudioNodeContent({
179
184
  element: PanelElement;
180
185
  selected?: boolean;
181
186
  }) {
187
+ const { t } = useI18n();
182
188
  const src = element.audioSrc || element.audioRemoteUrl || "";
183
189
  const audioRef = useRef<HTMLAudioElement | null>(null);
184
190
  const [playing, setPlaying] = useState(false);
@@ -269,11 +275,11 @@ function AudioNodeContent({
269
275
  el.pause();
270
276
  }
271
277
  }}
272
- title={!src ? "请先配置音频源" : playing ? "点击暂停" : "点击播放"}
278
+ title={!src ? t("panel.config.configureAudioFirst") : playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
273
279
  >
274
280
  <img
275
281
  src={poster}
276
- alt="音频占位图"
282
+ alt={t("panel.config.audioPoster")}
277
283
  className="h-full w-full object-cover"
278
284
  draggable={false}
279
285
  onDragStart={(e) => e.preventDefault()}
@@ -295,7 +301,7 @@ function AudioNodeContent({
295
301
  el.pause();
296
302
  }
297
303
  }}
298
- title={!src ? "请先配置音频源" : playing ? "点击暂停" : "点击播放"}
304
+ title={!src ? t("panel.config.configureAudioFirst") : playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
299
305
  >
300
306
  {renderIcon()}
301
307
  </button>
@@ -313,7 +319,7 @@ function AudioNodeContent({
313
319
  </>
314
320
  ) : null}
315
321
  <span className="pointer-events-none absolute right-2 top-2 rounded bg-black/55 px-1.5 py-0.5 text-[10px] text-white">
316
- {!src ? "未配置音频" : playing ? "暂停" : "播放"}
322
+ {!src ? t("panel.config.audioNotConfigured") : playing ? t("panel.config.pause") : t("panel.config.play")}
317
323
  </span>
318
324
  <audio ref={audioRef} src={src} preload="metadata" className="hidden" />
319
325
  </div>
@@ -322,14 +328,14 @@ function AudioNodeContent({
322
328
  if (!src) {
323
329
  return (
324
330
  <div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
325
- 音频占位
331
+ {t("panel.config.audioPlaceholder")}
326
332
  </div>
327
333
  );
328
334
  }
329
335
  return (
330
336
  <div className="relative flex h-full w-full items-center justify-center rounded border border-border/60 bg-muted/10 px-2">
331
337
  <div className="pointer-events-none text-[11px] text-muted-foreground">
332
- {playing ? "音频播放中" : "音频已就绪"}
338
+ {playing ? t("panel.config.audioPlaying") : t("panel.config.audioReady")}
333
339
  </div>
334
340
  <button
335
341
  type="button"
@@ -345,9 +351,9 @@ function AudioNodeContent({
345
351
  el.pause();
346
352
  }
347
353
  }}
348
- title={playing ? "点击暂停" : "点击播放"}
354
+ title={playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
349
355
  >
350
- {playing ? "暂停" : "播放"}
356
+ {playing ? t("panel.config.pause") : t("panel.config.play")}
351
357
  </button>
352
358
  <audio ref={audioRef} src={src} preload="metadata" className="hidden" />
353
359
  </div>
@@ -361,6 +367,7 @@ function VideoNodeContent({
361
367
  element: PanelElement;
362
368
  selected?: boolean;
363
369
  }) {
370
+ const { t } = useI18n();
364
371
  const src = element.videoSrc || element.videoRemoteUrl || "";
365
372
  const videoRef = useRef<HTMLVideoElement | null>(null);
366
373
  const [playing, setPlaying] = useState(false);
@@ -388,7 +395,7 @@ function VideoNodeContent({
388
395
  if (!src) {
389
396
  return (
390
397
  <div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
391
- 视频占位
398
+ {t("panel.config.videoPlaceholder")}
392
399
  </div>
393
400
  );
394
401
  }
@@ -409,9 +416,9 @@ function VideoNodeContent({
409
416
  el.pause();
410
417
  }
411
418
  }}
412
- title={playing ? "点击暂停" : "点击播放"}
419
+ title={playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
413
420
  >
414
- {playing ? "暂停" : "播放"}
421
+ {playing ? t("panel.config.pause") : t("panel.config.play")}
415
422
  </button>
416
423
  </div>
417
424
  );
@@ -423,20 +430,22 @@ function hasBackgroundImage(element: PanelElement) {
423
430
  }
424
431
 
425
432
  function ImageNodeContent({ element }: { element: PanelElement }) {
433
+ const { t } = useI18n();
426
434
  if (hasBackgroundImage(element)) return null;
427
435
  return (
428
436
  <div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
429
- 图片占位
437
+ {t("panel.config.imagePlaceholder")}
430
438
  </div>
431
439
  );
432
440
  }
433
441
 
434
442
  function EmptyNodePlaceholder({ element }: { element: PanelElement }) {
443
+ const { t } = useI18n();
435
444
  const labelMap: Record<string, string> = {
436
- video: "视频占位",
437
- audio: "音频占位",
445
+ video: t("panel.config.videoPlaceholder"),
446
+ audio: t("panel.config.audioPlaceholder"),
438
447
  };
439
- const label = labelMap[element.materialType ?? ""] ?? `${element.materialType ?? "节点"} 占位`;
448
+ const label = labelMap[element.materialType ?? ""] ?? t("panel.config.materialPlaceholder", { type: element.materialType ?? t("common.node") });
440
449
  return (
441
450
  <div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
442
451
  {label}
@@ -681,6 +690,7 @@ function ReferenceNodeContent({
681
690
  snapshotSource?: PanelElement[];
682
691
  visitedIds?: string[];
683
692
  }) {
693
+ const { t } = useI18n();
684
694
  const sourceNodes = useMemo(
685
695
  () => {
686
696
  const fromDeep =
@@ -725,7 +735,7 @@ function ReferenceNodeContent({
725
735
  }, [element.height, element.width, sourceNodes]);
726
736
 
727
737
  if (!layout || sourceNodes.length === 0) {
728
- const hintText = element.refLayerId ? "引用图层暂无节点" : "请在右侧选择引用图层";
738
+ const hintText = element.refLayerId ? t("panel.config.refLayerEmpty") : t("panel.config.selectRefLayer");
729
739
  return (
730
740
  <div className="flex h-full w-full items-center justify-center">
731
741
  <div className="rounded border border-dashed border-border/70 px-2 py-1 text-[10px] text-muted-foreground">
@@ -779,6 +789,8 @@ function ReferenceNodeContent({
779
789
  />
780
790
  ) : node.materialType === "geometry" ? (
781
791
  <GeometryNodeContent element={node} />
792
+ ) : node.materialType === "table" ? (
793
+ <TableNodeContent element={node} interactive={false} />
782
794
  ) : (
783
795
  <div className="h-full w-full" />
784
796
  )}
@@ -799,6 +811,7 @@ export const ElementsLayer = React.memo(function ElementsLayer({
799
811
  layerLocked = false,
800
812
  previewMode = false,
801
813
  previewLayoutKey,
814
+ onTableCellAction,
802
815
  }: ElementsLayerProps) {
803
816
  const sortedElements = useMemo(() => {
804
817
  return [...elements].sort((a, b) => {
@@ -877,6 +890,12 @@ export const ElementsLayer = React.memo(function ElementsLayer({
877
890
  <GeometryNodeContent element={el} />
878
891
  ) : el.materialType === "image" ? (
879
892
  <ImageNodeContent element={el} />
893
+ ) : el.materialType === "table" ? (
894
+ <TableNodeContent
895
+ element={el}
896
+ interactive={previewMode || Boolean(onTableCellAction)}
897
+ onCellAction={onTableCellAction}
898
+ />
880
899
  ) : (
881
900
  <EmptyNodePlaceholder element={el} />
882
901
  )}