@arronqzy/react-view 1.0.13 → 1.0.14

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 (29) hide show
  1. package/dist/styles.css +96 -4
  2. package/package.json +7 -6
  3. package/src/panel/ReactViewOnlinePreview.tsx +31 -2
  4. package/src/panel/ReactViewPanel.tsx +186 -124
  5. package/src/panel/ai/applyAssistantAction.ts +1 -0
  6. package/src/panel/components/BusyOverlay.tsx +25 -0
  7. package/src/panel/components/ElementsLayer.tsx +110 -68
  8. package/src/panel/components/MaterialSidebar.tsx +15 -0
  9. package/src/panel/components/MoveableLayer.tsx +8 -0
  10. package/src/panel/components/PanelCanvas.tsx +3 -0
  11. package/src/panel/components/PanelConfigSidebar.tsx +140 -91
  12. package/src/panel/components/SelectLayer.tsx +5 -0
  13. package/src/panel/components/WorkspaceConfigSidebar.tsx +1 -0
  14. package/src/panel/components/WorkspaceProjectNav.tsx +27 -4
  15. package/src/panel/components/config/PanelConfigScene3dSection.tsx +874 -0
  16. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +1 -2
  17. package/src/panel/components/table/PanelConfigTableSection.tsx +82 -50
  18. package/src/panel/components/table/TableNodeContent.tsx +40 -15
  19. package/src/panel/hooks/usePanelElements.ts +13 -1
  20. package/src/panel/hooks/useWorkspaceProjects.ts +133 -66
  21. package/src/panel/library/workspace-project-cache.ts +8 -2
  22. package/src/panel/library/workspace-project-db.ts +29 -11
  23. package/src/panel/library/workspace-snapshot.ts +21 -4
  24. package/src/panel/scope/view-scope-store.ts +8 -0
  25. package/src/panel/types.ts +4 -3
  26. package/src/panel/utils/async-work.ts +178 -0
  27. package/src/panel/utils/gridPlacement.ts +29 -0
  28. package/src/panel/utils/panelElementDefaults.ts +37 -1
  29. package/src/panel/utils/scope-autocomplete.ts +0 -114
@@ -5,6 +5,17 @@ import type { PanelElement } from "../types";
5
5
  import { buildChartOption, CHART_TYPES } from "../utils/chartOptionBuilder";
6
6
  import { PREVIEW_LAYOUT_EVENT } from "../utils/panelStateIO";
7
7
  import { TableNodeContent, type TableCellActionHandler } from "./table/TableNodeContent";
8
+ import { comparePanelElementsPaintOrder } from "../utils/gridPlacement";
9
+ import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
10
+ import { mergeScene3dConfig } from "@arronqzy/view-scene3d";
11
+ import { Scene3dNodeContent } from "@arronqzy/view-scene3d/react";
12
+ import {
13
+ createViewEventSignal,
14
+ snapshotDomEvent,
15
+ snapshotViewEventNode,
16
+ type ViewEventSignal,
17
+ type ViewEventType,
18
+ } from "@arronqzy/blueprint-dsl";
8
19
 
9
20
  export type ElementsLayerProps = {
10
21
  elements: PanelElement[];
@@ -22,6 +33,8 @@ export type ElementsLayerProps = {
22
33
  /** 预览布局变更时递增,用于触发图表/画布重绘 */
23
34
  previewLayoutKey?: number;
24
35
  onTableCellAction?: TableCellActionHandler;
36
+ boundViewEventTypes?: ReadonlyMap<string, ReadonlySet<ViewEventType>>;
37
+ onViewUiEvent?: (payload: ViewEventSignal) => void;
25
38
  };
26
39
 
27
40
  function TextNodeContent({
@@ -41,8 +54,8 @@ function TextNodeContent({
41
54
  fontSize: element.textFontSize ? `${element.textFontSize}px` : undefined,
42
55
  fontWeight: element.textFontWeight || undefined,
43
56
  color: element.textColor || undefined,
44
- lineHeight: element.textLineHeight ? String(element.textLineHeight) : undefined,
45
- textAlign: element.textAlign ?? "left",
57
+ lineHeight: cssTextLineHeight(element.textLineHeight),
58
+ ...cssTextAlignStyle(element.textAlign),
46
59
  };
47
60
 
48
61
  useEffect(() => {
@@ -56,7 +69,8 @@ function TextNodeContent({
56
69
  return (
57
70
  <div
58
71
  ref={ref}
59
- className="h-full w-full overflow-auto break-words p-2 text-sm leading-relaxed outline-none"
72
+ data-panel-user-text=""
73
+ className="h-full w-full overflow-auto break-words p-2 outline-none"
60
74
  style={textStyle}
61
75
  contentEditable={editable}
62
76
  suppressContentEditableWarning
@@ -99,34 +113,23 @@ function GridNodeContent({
99
113
  });
100
114
  return set;
101
115
  }, [allElements, cols, element.id, element.layerId, rows]);
102
- const occupiedBlocks = useMemo(() => {
103
- return allElements
104
- .filter(
105
- (el) =>
106
- el.parentGridId === element.id &&
107
- el.layerId === element.layerId &&
108
- el.gridSlotIndex !== undefined
109
- )
110
- .map((el) => {
111
- const start = Math.max(0, Math.floor(el.gridSlotIndex ?? 0));
112
- const baseRow = Math.floor(start / cols);
113
- const baseCol = start % cols;
114
- const rowSpan = Math.max(1, Math.min(rows - baseRow, Math.floor(el.gridRowSpan ?? 1)));
115
- const colSpan = Math.max(1, Math.min(cols - baseCol, Math.floor(el.gridColSpan ?? 1)));
116
- return {
117
- id: el.id,
118
- rowStart: baseRow + 1,
119
- rowEnd: baseRow + rowSpan + 1,
120
- colStart: baseCol + 1,
121
- colEnd: baseCol + colSpan + 1,
122
- };
123
- });
124
- }, [allElements, cols, element.id, element.layerId, rows]);
125
116
  if (previewMode) {
126
117
  return <div className="relative h-full w-full" />;
127
118
  }
119
+ const total = rows * cols;
120
+ const emptyCount = total - occupied.size;
128
121
  return (
129
- <div className="relative h-full w-full">
122
+ <div
123
+ className="pointer-events-none relative h-full w-full overflow-hidden rounded-md"
124
+ style={
125
+ emptyCount > 0
126
+ ? {
127
+ border: "2px dashed rgba(14, 165, 233, 0.7)",
128
+ background: "rgba(14, 165, 233, 0.08)",
129
+ }
130
+ : undefined
131
+ }
132
+ >
130
133
  <div
131
134
  className="h-full w-full"
132
135
  style={{
@@ -138,41 +141,41 @@ function GridNodeContent({
138
141
  boxSizing: "border-box",
139
142
  }}
140
143
  >
141
- {Array.from({ length: rows * cols }).map((_, idx) => (
142
- <div
143
- key={idx}
144
- className={[
145
- "rounded border border-dashed",
146
- occupied.has(idx)
147
- ? "border-primary/70 bg-primary/10"
148
- : "border-border/60 bg-muted/20",
149
- ].join(" ")}
150
- title={occupied.has(idx) ? t("panel.config.slotOccupied", { n: idx + 1 }) : t("panel.config.slotEmpty", { n: idx + 1 })}
151
- />
152
- ))}
153
- </div>
154
- <div
155
- className="pointer-events-none absolute inset-0"
156
- style={{
157
- display: "grid",
158
- gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
159
- gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
160
- gap: `${gap}px`,
161
- padding: `${padding}px`,
162
- boxSizing: "border-box",
163
- }}
164
- >
165
- {occupiedBlocks.map((block) => (
166
- <div
167
- key={block.id}
168
- className="rounded border border-primary/70 bg-primary/20"
169
- style={{
170
- gridColumn: `${block.colStart} / ${block.colEnd}`,
171
- gridRow: `${block.rowStart} / ${block.rowEnd}`,
172
- }}
173
- />
174
- ))}
144
+ {Array.from({ length: total }).map((_, idx) => {
145
+ const filled = occupied.has(idx);
146
+ return (
147
+ <div
148
+ key={idx}
149
+ className={
150
+ filled
151
+ ? "pointer-events-none"
152
+ : "pointer-events-auto flex flex-col items-center justify-center gap-0.5 rounded-md text-[11px] font-semibold"
153
+ }
154
+ style={
155
+ filled
156
+ ? undefined
157
+ : {
158
+ border: "2px dashed rgba(2, 132, 199, 0.85)",
159
+ background: "rgba(186, 230, 253, 0.72)",
160
+ color: "rgba(3, 105, 161, 0.95)",
161
+ }
162
+ }
163
+ title={
164
+ filled
165
+ ? t("panel.config.slotOccupied", { n: idx + 1 })
166
+ : t("panel.config.slotEmpty", { n: idx + 1 })
167
+ }
168
+ >
169
+ {filled ? null : <span>{idx + 1}</span>}
170
+ </div>
171
+ );
172
+ })}
175
173
  </div>
174
+ {occupied.size === 0 ? (
175
+ <div className="pointer-events-none absolute bottom-1.5 left-0 right-0 text-center text-[11px] font-medium text-sky-800/90">
176
+ {t("panel.config.gridDropHint")}
177
+ </div>
178
+ ) : null}
176
179
  </div>
177
180
  );
178
181
  }
@@ -789,6 +792,8 @@ function ReferenceNodeContent({
789
792
  />
790
793
  ) : node.materialType === "geometry" ? (
791
794
  <GeometryNodeContent element={node} />
795
+ ) : node.materialType === "scene3d" ? (
796
+ <Scene3dNodeContent config={node.scene3d} previewMode />
792
797
  ) : node.materialType === "table" ? (
793
798
  <TableNodeContent element={node} interactive={false} />
794
799
  ) : (
@@ -812,19 +817,34 @@ export const ElementsLayer = React.memo(function ElementsLayer({
812
817
  previewMode = false,
813
818
  previewLayoutKey,
814
819
  onTableCellAction,
820
+ boundViewEventTypes,
821
+ onViewUiEvent,
815
822
  }: ElementsLayerProps) {
823
+ const emitViewUiEvent = useCallback(
824
+ (el: PanelElement, eventType: ViewEventType, domEvent: React.MouseEvent | MouseEvent) => {
825
+ if (!onViewUiEvent) return;
826
+ if (!boundViewEventTypes?.get(el.id)?.has(eventType)) return;
827
+ onViewUiEvent(
828
+ createViewEventSignal({
829
+ eventType,
830
+ event: snapshotDomEvent(domEvent),
831
+ node: snapshotViewEventNode(el),
832
+ })
833
+ );
834
+ },
835
+ [boundViewEventTypes, onViewUiEvent]
836
+ );
816
837
  const sortedElements = useMemo(() => {
817
- return [...elements].sort((a, b) => {
818
- const za = a.zIndex ?? 1;
819
- const zb = b.zIndex ?? 1;
820
- if (za !== zb) return za - zb;
821
- return a.id.localeCompare(b.id);
822
- });
823
- }, [elements]);
838
+ const byId = new Map<string, PanelElement>();
839
+ for (const el of allElements) byId.set(el.id, el);
840
+ return [...elements].sort((a, b) => comparePanelElementsPaintOrder(a, b, byId));
841
+ }, [allElements, elements]);
824
842
  return (
825
843
  <>
826
844
  {sortedElements.map((el) => {
827
845
  const isSelected = selectedIds.includes(el.id);
846
+ const boundTypes = boundViewEventTypes?.get(el.id);
847
+ const hasViewEvent = Boolean(boundTypes && boundTypes.size > 0 && onViewUiEvent);
828
848
  return (
829
849
  <div
830
850
  key={el.id}
@@ -832,10 +852,16 @@ export const ElementsLayer = React.memo(function ElementsLayer({
832
852
  "absolute select-none",
833
853
  previewMode ? "" : "rv-selectable",
834
854
  !previewMode && isSelected ? "ring-2 ring-blue-500/90 ring-offset-0" : "",
855
+ hasViewEvent ? "cursor-pointer" : "",
835
856
  ].join(" ")}
836
857
  data-element-id={el.id}
837
858
  onMouseDown={(e) => {
838
859
  if (previewMode) return;
860
+ const target = e.target as HTMLElement | null;
861
+ if (target?.closest("[data-scene3d-orbit-active='true']")) {
862
+ e.stopPropagation();
863
+ return;
864
+ }
839
865
  if (e.button !== 0) return;
840
866
  // 单击选中(与 Selecto 的框选互补)
841
867
  if (e.shiftKey) {
@@ -848,6 +874,11 @@ export const ElementsLayer = React.memo(function ElementsLayer({
848
874
  onSelectIds([el.id]);
849
875
  }
850
876
  }}
877
+ onClick={(e) => emitViewUiEvent(el, "click", e)}
878
+ onDoubleClick={(e) => emitViewUiEvent(el, "dblclick", e)}
879
+ onContextMenu={(e) => emitViewUiEvent(el, "contextmenu", e)}
880
+ onMouseEnter={(e) => emitViewUiEvent(el, "mouseenter", e)}
881
+ onMouseLeave={(e) => emitViewUiEvent(el, "mouseleave", e)}
851
882
  style={{
852
883
  left: el.x,
853
884
  top: el.y,
@@ -888,6 +919,17 @@ export const ElementsLayer = React.memo(function ElementsLayer({
888
919
  <VideoNodeContent element={el} selected={isSelected} />
889
920
  ) : el.materialType === "geometry" ? (
890
921
  <GeometryNodeContent element={el} />
922
+ ) : el.materialType === "scene3d" ? (
923
+ <Scene3dNodeContent
924
+ config={el.scene3d}
925
+ previewMode={previewMode}
926
+ selected={isSelected}
927
+ updateConfig={(patch) =>
928
+ updateElement(el.id, {
929
+ scene3d: mergeScene3dConfig({ ...el.scene3d, ...patch }),
930
+ })
931
+ }
932
+ />
891
933
  ) : el.materialType === "image" ? (
892
934
  <ImageNodeContent element={el} />
893
935
  ) : el.materialType === "table" ? (
@@ -68,6 +68,7 @@ const MATERIAL_LABEL_KEYS: Record<string, string> = {
68
68
  audio: "panel.material.audio",
69
69
  reference: "panel.material.reference",
70
70
  geometry: "panel.material.geometry",
71
+ scene3d: "panel.material.scene3d",
71
72
  table: "panel.material.table",
72
73
  };
73
74
 
@@ -298,6 +299,19 @@ function MaterialPreview({ id }: { id: string }) {
298
299
  );
299
300
  }
300
301
 
302
+ if (id === "scene3d") {
303
+ return (
304
+ <div className={common}>
305
+ <svg viewBox="0 0 80 60" className="h-full w-full">
306
+ <polygon points="40,8 62,22 62,44 40,56 18,44 18,22" className="fill-primary/20 stroke-primary/80" strokeWidth="1.5" />
307
+ <polygon points="40,8 62,22 40,32 18,22" className="fill-primary/45" />
308
+ <polygon points="40,32 62,22 62,44 40,56" className="fill-primary/30" />
309
+ <polygon points="40,32 18,22 18,44 40,56" className="fill-primary/55" />
310
+ </svg>
311
+ </div>
312
+ );
313
+ }
314
+
301
315
  if (id === "reference") {
302
316
  return (
303
317
  <div className={common}>
@@ -387,6 +401,7 @@ function getDefaultCategories(t: (key: string) => string): MaterialCategory[] {
387
401
  { id: "text", title: t("panel.material.text") },
388
402
  { id: "table", title: t("panel.material.table") },
389
403
  { id: "geometry", title: t("panel.material.geometry") },
404
+ { id: "scene3d", title: t("panel.material.scene3d") },
390
405
  { id: "grid", title: t("panel.material.grid") },
391
406
  { id: "image", title: t("panel.material.image") },
392
407
  { id: "reference", title: t("panel.material.reference") },
@@ -497,6 +497,10 @@ export function MoveableLayer({
497
497
  throttleResize={0}
498
498
  throttleRotate={0}
499
499
  onDragStart={(e: any) => {
500
+ if ((e.target as HTMLElement | null)?.closest("[data-scene3d-orbit-active='true']")) {
501
+ e.stop?.();
502
+ return;
503
+ }
500
504
  const id = resolveSingleEventId(e.target as HTMLElement | null);
501
505
  if (!id) return;
502
506
  const data = elementsById.get(id);
@@ -528,6 +532,10 @@ export function MoveableLayer({
528
532
  e.target.style.top = `${sy + ty}px`;
529
533
  }}
530
534
  onDragGroupStart={(e: any) => {
535
+ if ((e.target as HTMLElement | null)?.closest("[data-scene3d-orbit-active='true']")) {
536
+ e.stop?.();
537
+ return;
538
+ }
531
539
  e.events.forEach((ev: any) => {
532
540
  const id = ev.target ? getId(ev.target) : null;
533
541
  if (!id) return;
@@ -172,6 +172,7 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
172
172
  if (!viewer) return;
173
173
  const target = e.target as HTMLElement | null;
174
174
  if (
175
+ target?.closest("[data-scene3d-orbit-active='true']") ||
175
176
  target?.closest("input, textarea, select, [contenteditable='true']") ||
176
177
  target?.closest("[role='dialog']")
177
178
  ) {
@@ -448,6 +449,8 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
448
449
  }
449
450
  };
450
451
  const onMouseDown = (e: MouseEvent) => {
452
+ const target = e.target as HTMLElement | null;
453
+ if (target?.closest("[data-scene3d-orbit-active='true']")) return;
451
454
  // 右键 或 mac 上 ctrl+左键
452
455
  const isRightLike = e.button === 2 || (e.button === 0 && e.ctrlKey);
453
456
  if (!isRightLike) return;