@arronqzy/react-view 1.0.12 → 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.
- package/dist/styles.css +96 -4
- package/package.json +8 -7
- package/src/panel/ReactViewOnlinePreview.tsx +72 -20
- package/src/panel/ReactViewPanel.tsx +238 -126
- package/src/panel/ai/applyAssistantAction.ts +1 -0
- package/src/panel/components/BusyOverlay.tsx +25 -0
- package/src/panel/components/ElementsLayer.tsx +110 -68
- package/src/panel/components/MaterialSidebar.tsx +15 -0
- package/src/panel/components/MoveableLayer.tsx +8 -0
- package/src/panel/components/PanelCanvas.tsx +3 -0
- package/src/panel/components/PanelConfigSidebar.tsx +582 -443
- package/src/panel/components/SelectLayer.tsx +5 -0
- package/src/panel/components/WorkspaceConfigSidebar.tsx +9 -0
- package/src/panel/components/WorkspaceProjectNav.tsx +27 -4
- package/src/panel/components/config/PanelConfigScene3dSection.tsx +874 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +1 -2
- package/src/panel/components/table/PanelConfigTableSection.tsx +82 -50
- package/src/panel/components/table/TableNodeContent.tsx +40 -15
- package/src/panel/hooks/usePanelElements.ts +13 -1
- package/src/panel/hooks/useWorkspaceProjects.ts +133 -66
- package/src/panel/library/workspace-project-cache.ts +8 -2
- package/src/panel/library/workspace-project-db.ts +29 -11
- package/src/panel/library/workspace-snapshot.ts +21 -4
- package/src/panel/scope/view-scope-store.ts +8 -0
- package/src/panel/types.ts +4 -3
- package/src/panel/utils/async-work.ts +178 -0
- package/src/panel/utils/chartOptionBuilder.ts +7 -1
- package/src/panel/utils/gridPlacement.ts +29 -0
- package/src/panel/utils/outputScale.ts +69 -0
- package/src/panel/utils/panelElementDefaults.ts +37 -2
- 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:
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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;
|