@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.
Files changed (31) hide show
  1. package/dist/styles.css +96 -4
  2. package/package.json +8 -7
  3. package/src/panel/ReactViewOnlinePreview.tsx +72 -20
  4. package/src/panel/ReactViewPanel.tsx +238 -126
  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 +582 -443
  12. package/src/panel/components/SelectLayer.tsx +5 -0
  13. package/src/panel/components/WorkspaceConfigSidebar.tsx +9 -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/chartOptionBuilder.ts +7 -1
  28. package/src/panel/utils/gridPlacement.ts +29 -0
  29. package/src/panel/utils/outputScale.ts +69 -0
  30. package/src/panel/utils/panelElementDefaults.ts +37 -2
  31. package/src/panel/utils/scope-autocomplete.ts +0 -114
@@ -7,6 +7,7 @@ import { usePanelElements } from "./hooks/usePanelElements";
7
7
  import { useWorkspaceProjects } from "./hooks/useWorkspaceProjects";
8
8
  import type { WorkspaceProjectRecord } from "./library/workspace-project-db";
9
9
  import { WorkspaceProjectNav } from "./components/WorkspaceProjectNav";
10
+ import { BusyOverlay } from "./components/BusyOverlay";
10
11
  import { PanelCanvas } from "./components/PanelCanvas";
11
12
  import { PanelRulers } from "./components/PanelRulers";
12
13
  import { type ViewportZoom } from "./viewportZoom";
@@ -14,6 +15,13 @@ import { ElementsLayer } from "./components/ElementsLayer";
14
15
  import { SelectLayer } from "./components/SelectLayer";
15
16
  import { MoveableLayer } from "./components/MoveableLayer";
16
17
  import { buildChartOption, CHART_TYPES } from "./utils/chartOptionBuilder";
18
+ import { readOutputScale, writeOutputScale } from "./utils/outputScale";
19
+ import {
20
+ assertFileSize,
21
+ parseJsonText,
22
+ readFileAsDataUrl,
23
+ runBusyTask,
24
+ } from "./utils/async-work";
17
25
  import { MaterialSidebar } from "./components/MaterialSidebar";
18
26
  import { AssistantChatPanel } from "./ai/AssistantChatPanel";
19
27
  import {
@@ -44,13 +52,15 @@ import {
44
52
  useBlueprintNodeSelectionGuard,
45
53
  useBlueprintPageLifecycle,
46
54
  stopAllClockSchedules,
55
+ resolveRunnableNodeType,
56
+ resolveViewElementIds,
47
57
  type BlueprintDocument,
48
58
  type BlueprintGraphNode,
49
59
  type BlueprintLibraryListItem,
50
60
  type BlueprintMetaDraft,
51
61
  } from "@arronqzy/react-blueprint";
52
62
  import type { LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
53
- import { abortClockNode } from "@arronqzy/blueprint-dsl";
63
+ import { abortClockNode, collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE } from "@arronqzy/blueprint-dsl";
54
64
  import { WorkspaceStageSplit } from "./components/WorkspaceStageSplit";
55
65
  import {
56
66
  WorkspaceConfigSidebar,
@@ -94,6 +104,7 @@ import {
94
104
  EmptyTitle,
95
105
  Input,
96
106
  Menubar,
107
+ MenubarCheckboxItem,
97
108
  MenubarContent,
98
109
  MenubarItem,
99
110
  MenubarMenu,
@@ -460,6 +471,7 @@ export function ReactViewPanel({
460
471
  const [titleIconZoom, setTitleIconZoom] = useState(1.6);
461
472
  const [pendingPreviewLayerId, setPendingPreviewLayerId] = useState<string | null>(null);
462
473
  const [panelFontSize, setPanelFontSize] = useState<"sm" | "md" | "lg">("md");
474
+ const [outputScale, setOutputScale] = useState(() => readOutputScale());
463
475
  const [blueprintOpen, setBlueprintOpen] = useState(false);
464
476
  const [assistantOpen, setAssistantOpen] = useState(false);
465
477
  const [blueprintGraph, setBlueprintGraph] = useState(() => BlueprintGraph.empty());
@@ -570,6 +582,10 @@ export function ReactViewPanel({
570
582
  }
571
583
  }, [panelFontSize]);
572
584
 
585
+ useEffect(() => {
586
+ writeOutputScale(outputScale);
587
+ }, [outputScale]);
588
+
573
589
  useEffect(() => {
574
590
  try {
575
591
  const stored = localStorage.getItem(TITLE_ICON_STORAGE_KEY) ?? "";
@@ -729,6 +745,7 @@ export function ReactViewPanel({
729
745
  | "jsonConfig"
730
746
  | "logicConfig"
731
747
  | "clockConfig"
748
+ | "eventConfig"
732
749
  >
733
750
  >
734
751
  ) => {
@@ -980,18 +997,20 @@ export function ReactViewPanel({
980
997
  void refreshBlueprintLibrary();
981
998
  return;
982
999
  }
983
- setBlueprintGraph(BlueprintGraph.fromDocument(record.document));
984
- setBlueprintMeta({
985
- name: record.name,
986
- remark: record.remark ?? "",
1000
+ await runBusyTask(t("common.loadingBlueprint"), async () => {
1001
+ setBlueprintGraph(BlueprintGraph.fromDocument(record.document));
1002
+ setBlueprintMeta({
1003
+ name: record.name,
1004
+ remark: record.remark ?? "",
1005
+ });
1006
+ setActiveBlueprintLibraryId(record.id);
1007
+ setBlueprintSyncedDocument(record.document);
1008
+ setSelectedBlueprintNodeId(null);
1009
+ setConfigFocus("blueprint");
1010
+ setBlueprintOpen(true);
987
1011
  });
988
- setActiveBlueprintLibraryId(record.id);
989
- setBlueprintSyncedDocument(record.document);
990
- setSelectedBlueprintNodeId(null);
991
- setConfigFocus("blueprint");
992
- setBlueprintOpen(true);
993
1012
  },
994
- [refreshBlueprintLibrary]
1013
+ [refreshBlueprintLibrary, t]
995
1014
  );
996
1015
 
997
1016
  const snapshotWorkspaceBlueprint = useCallback(() => {
@@ -1045,66 +1064,70 @@ export function ReactViewPanel({
1045
1064
 
1046
1065
  const syncBlueprintToLibrary = useCallback(async () => {
1047
1066
  if (!activeBlueprintLibraryId) return;
1067
+ await runBusyTask(t("common.syncingBlueprint"), async () => {
1068
+ const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1069
+ if (!existing) {
1070
+ toast({ title: t("panel.messages.blueprintNotFound") });
1071
+ void refreshBlueprintLibrary();
1072
+ setActiveBlueprintLibraryId(null);
1073
+ setBlueprintSyncedDocument(null);
1074
+ return;
1075
+ }
1048
1076
 
1049
- const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1050
- if (!existing) {
1051
- toast({ title: t("panel.messages.blueprintNotFound") });
1052
- void refreshBlueprintLibrary();
1053
- setActiveBlueprintLibraryId(null);
1054
- setBlueprintSyncedDocument(null);
1055
- return;
1056
- }
1057
-
1058
- const record = buildLibraryRecord({
1059
- id: existing.id,
1060
- createdAt: existing.createdAt,
1061
- document: blueprintGraph.document,
1062
- meta: blueprintMeta,
1063
- source: existing.source,
1077
+ const record = buildLibraryRecord({
1078
+ id: existing.id,
1079
+ createdAt: existing.createdAt,
1080
+ document: blueprintGraph.document,
1081
+ meta: blueprintMeta,
1082
+ source: existing.source,
1083
+ });
1084
+ await putBlueprintLibraryRecord(record);
1085
+ setBlueprintSyncedDocument(blueprintGraph.document);
1086
+ await refreshBlueprintLibrary();
1087
+ toast({ title: messages.blueprintSynced(record.name) });
1064
1088
  });
1065
- await putBlueprintLibraryRecord(record);
1066
- setBlueprintSyncedDocument(blueprintGraph.document);
1067
- await refreshBlueprintLibrary();
1068
- toast({ title: messages.blueprintSynced(record.name) });
1069
1089
  }, [
1070
1090
  activeBlueprintLibraryId,
1071
1091
  blueprintGraph.document,
1072
1092
  blueprintMeta,
1073
1093
  refreshBlueprintLibrary,
1094
+ t,
1074
1095
  ]);
1075
1096
 
1076
1097
  const saveBlueprintToLibrary = useCallback(
1077
1098
  async (meta: BlueprintMetaDraft) => {
1078
- setBlueprintMeta(meta);
1079
-
1080
- let recordId = activeBlueprintLibraryId ?? undefined;
1081
- let createdAt: number | undefined;
1082
- if (activeBlueprintLibraryId) {
1083
- const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1084
- if (existing?.source === "saved") {
1085
- recordId = existing.id;
1086
- createdAt = existing.createdAt;
1099
+ await runBusyTask(t("common.savingBlueprint"), async () => {
1100
+ setBlueprintMeta(meta);
1101
+
1102
+ let recordId = activeBlueprintLibraryId ?? undefined;
1103
+ let createdAt: number | undefined;
1104
+ if (activeBlueprintLibraryId) {
1105
+ const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1106
+ if (existing?.source === "saved") {
1107
+ recordId = existing.id;
1108
+ createdAt = existing.createdAt;
1109
+ } else {
1110
+ recordId = createLibraryBlueprintId();
1111
+ }
1087
1112
  } else {
1088
1113
  recordId = createLibraryBlueprintId();
1089
1114
  }
1090
- } else {
1091
- recordId = createLibraryBlueprintId();
1092
- }
1093
1115
 
1094
- const record = buildLibraryRecord({
1095
- id: recordId,
1096
- createdAt,
1097
- document: blueprintGraph.document,
1098
- meta,
1099
- source: "saved",
1116
+ const record = buildLibraryRecord({
1117
+ id: recordId,
1118
+ createdAt,
1119
+ document: blueprintGraph.document,
1120
+ meta,
1121
+ source: "saved",
1122
+ });
1123
+ await putBlueprintLibraryRecord(record);
1124
+ setActiveBlueprintLibraryId(record.id);
1125
+ setBlueprintSyncedDocument(blueprintGraph.document);
1126
+ await refreshBlueprintLibrary();
1127
+ toast({ title: t("panel.messages.blueprintSavedLocal") });
1100
1128
  });
1101
- await putBlueprintLibraryRecord(record);
1102
- setActiveBlueprintLibraryId(record.id);
1103
- setBlueprintSyncedDocument(blueprintGraph.document);
1104
- await refreshBlueprintLibrary();
1105
- toast({ title: t("panel.messages.blueprintSavedLocal") });
1106
1129
  },
1107
- [activeBlueprintLibraryId, blueprintGraph.document, refreshBlueprintLibrary]
1130
+ [activeBlueprintLibraryId, blueprintGraph.document, refreshBlueprintLibrary, t]
1108
1131
  );
1109
1132
 
1110
1133
  const openBlueprintMetaDialog = useCallback((mode: "export" | "save") => {
@@ -1115,16 +1138,18 @@ export function ReactViewPanel({
1115
1138
  const handleBlueprintMetaConfirm = useCallback(
1116
1139
  async (meta: BlueprintMetaDraft) => {
1117
1140
  if (blueprintMetaDialogMode === "export") {
1118
- setBlueprintMeta(meta);
1119
- downloadBlueprintExport(
1120
- buildBlueprintExportPayload(blueprintGraph.document, meta)
1121
- );
1122
- toast({ title: t("panel.messages.blueprintExported") });
1141
+ await runBusyTask(t("common.exportingPanel"), async () => {
1142
+ setBlueprintMeta(meta);
1143
+ downloadBlueprintExport(
1144
+ buildBlueprintExportPayload(blueprintGraph.document, meta)
1145
+ );
1146
+ toast({ title: t("panel.messages.blueprintExported") });
1147
+ });
1123
1148
  return;
1124
1149
  }
1125
1150
  await saveBlueprintToLibrary(meta);
1126
1151
  },
1127
- [blueprintGraph.document, blueprintMetaDialogMode, saveBlueprintToLibrary]
1152
+ [blueprintGraph.document, blueprintMetaDialogMode, saveBlueprintToLibrary, t]
1128
1153
  );
1129
1154
 
1130
1155
  const handleRenameBlueprintLibraryItem = useCallback(
@@ -1163,22 +1188,32 @@ export function ReactViewPanel({
1163
1188
 
1164
1189
  const handleBlueprintImportFile = useCallback(
1165
1190
  async (file: File) => {
1166
- const text = await file.text();
1167
- const payload = parseBlueprintImportFile(JSON.parse(text) as unknown);
1168
- const record = libraryRecordFromImport(payload);
1169
- await putBlueprintLibraryRecord(record);
1170
- await refreshBlueprintLibrary();
1171
- if (!activeBlueprintLibraryId) {
1172
- snapshotWorkspaceBlueprint();
1191
+ try {
1192
+ await runBusyTask(t("common.importingBlueprint"), async () => {
1193
+ assertFileSize(file, "json");
1194
+ const text = await file.text();
1195
+ const payload = parseBlueprintImportFile(await parseJsonText(text));
1196
+ const record = libraryRecordFromImport(payload);
1197
+ await putBlueprintLibraryRecord(record);
1198
+ await refreshBlueprintLibrary();
1199
+ if (!activeBlueprintLibraryId) {
1200
+ snapshotWorkspaceBlueprint();
1201
+ }
1202
+ await loadBlueprintFromLibrary(record.id);
1203
+ toast({ title: t("panel.messages.blueprintImported") });
1204
+ });
1205
+ } catch (error) {
1206
+ toast({
1207
+ title: error instanceof Error ? error.message : t("panel.messages.importInvalidFormat"),
1208
+ });
1173
1209
  }
1174
- await loadBlueprintFromLibrary(record.id);
1175
- toast({ title: t("panel.messages.blueprintImported") });
1176
1210
  },
1177
1211
  [
1178
1212
  activeBlueprintLibraryId,
1179
1213
  loadBlueprintFromLibrary,
1180
1214
  refreshBlueprintLibrary,
1181
1215
  snapshotWorkspaceBlueprint,
1216
+ t,
1182
1217
  ]
1183
1218
  );
1184
1219
 
@@ -1220,7 +1255,7 @@ export function ReactViewPanel({
1220
1255
  onProjectApplied: handleWorkspaceProjectApplied,
1221
1256
  });
1222
1257
 
1223
- const { triggerBlueprintNode } = useBlueprintPageLifecycle({
1258
+ const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
1224
1259
  graph: blueprintGraph,
1225
1260
  active: blueprintOpen,
1226
1261
  bootKey: workspaceProjects.activeProjectId ?? undefined,
@@ -1232,6 +1267,24 @@ export function ReactViewPanel({
1232
1267
  onViewScopeUpdate: handleViewScopeUpdate,
1233
1268
  });
1234
1269
 
1270
+ const boundViewEventTypes = useMemo(
1271
+ () =>
1272
+ collectArmedViewEventBindings(
1273
+ blueprintGraph.document.nodes.map((node) => ({
1274
+ id: node.id,
1275
+ nodeType: resolveRunnableNodeType(node),
1276
+ lifecyclePhase: node.lifecyclePhase,
1277
+ viewElementIds: resolveViewElementIds(node),
1278
+ eventConfig: node.eventConfig,
1279
+ })),
1280
+ blueprintGraph.document.edges,
1281
+ EVENT_NODE_TYPE,
1282
+ LIFECYCLE_NODE_TYPE,
1283
+ firedLifecyclePhases
1284
+ ),
1285
+ [blueprintGraph.document.nodes, blueprintGraph.document.edges, firedLifecyclePhases]
1286
+ );
1287
+
1235
1288
  const handleWorkspaceCreateProject = useCallback(async () => {
1236
1289
  const result = await workspaceProjects.handleCreateProject();
1237
1290
  if (result?.name) {
@@ -1243,6 +1296,7 @@ export function ReactViewPanel({
1243
1296
  const handleWorkspaceOpenProject = useCallback(
1244
1297
  async (id: string) => {
1245
1298
  try {
1299
+ stopAllClockSchedules();
1246
1300
  clearViewElementScopes();
1247
1301
  await workspaceProjects.handleOpenProject(id);
1248
1302
  setSelectedIds([]);
@@ -1556,70 +1610,72 @@ export function ReactViewPanel({
1556
1610
  );
1557
1611
 
1558
1612
  const handleExport = useCallback(() => {
1559
- const data = exportPanelData();
1560
- const blob = new Blob([JSON.stringify(data, null, 2)], {
1561
- type: "application/json",
1613
+ void runBusyTask(t("common.exportingPanel"), async () => {
1614
+ const data = exportPanelData();
1615
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
1616
+ type: "application/json",
1617
+ });
1618
+ const url = URL.createObjectURL(blob);
1619
+ const a = document.createElement("a");
1620
+ a.href = url;
1621
+ const safeName = (productName.trim() || "panel")
1622
+ .replace(/[\\/:*?"<>|]/g, "-")
1623
+ .replace(/\s+/g, "-");
1624
+ a.download = `${safeName}-${Date.now()}.json`;
1625
+ a.click();
1626
+ URL.revokeObjectURL(url);
1562
1627
  });
1563
- const url = URL.createObjectURL(blob);
1564
- const a = document.createElement("a");
1565
- a.href = url;
1566
- const safeName = (productName.trim() || "panel")
1567
- .replace(/[\\/:*?"<>|]/g, "-")
1568
- .replace(/\s+/g, "-");
1569
- a.download = `${safeName}-${Date.now()}.json`;
1570
- a.click();
1571
- URL.revokeObjectURL(url);
1572
- }, [exportPanelData, productName]);
1628
+ }, [exportPanelData, productName, t]);
1573
1629
 
1574
1630
  const handleImportFile = useCallback(
1575
1631
  async (file: File) => {
1576
- const text = await file.text();
1577
- const parsed = JSON.parse(text) as State;
1578
- const ok = importPanelData(parsed);
1579
- if (!ok) {
1580
- window.alert(messages.importInvalidFormat);
1581
- } else {
1582
- setSelectedIds([]);
1632
+ try {
1633
+ await runBusyTask(t("common.importingPanel"), async () => {
1634
+ assertFileSize(file, "json");
1635
+ const text = await file.text();
1636
+ const parsed = await parseJsonText<State>(text);
1637
+ const ok = importPanelData(parsed);
1638
+ if (!ok) {
1639
+ window.alert(messages.importInvalidFormat);
1640
+ } else {
1641
+ setSelectedIds([]);
1642
+ }
1643
+ });
1644
+ } catch (error) {
1645
+ window.alert(
1646
+ error instanceof Error ? error.message : messages.importInvalidFormat
1647
+ );
1583
1648
  }
1584
1649
  },
1585
- [importPanelData]
1650
+ [importPanelData, t]
1586
1651
  );
1587
1652
 
1588
- const normalizeTitleIconFile = useCallback((file: File) => {
1653
+ const normalizeTitleIconFile = useCallback(async (file: File) => {
1654
+ const src = await readFileAsDataUrl(file, "read-failed", "image");
1589
1655
  return new Promise<string>((resolve, reject) => {
1590
- const reader = new FileReader();
1591
- reader.onerror = () => reject(new Error("read-failed"));
1592
- reader.onload = () => {
1593
- const src = typeof reader.result === "string" ? reader.result : "";
1594
- if (!src) {
1595
- reject(new Error("empty-data"));
1656
+ const img = new Image();
1657
+ img.onload = () => {
1658
+ const size = 64;
1659
+ const canvas = document.createElement("canvas");
1660
+ canvas.width = size;
1661
+ canvas.height = size;
1662
+ const ctx = canvas.getContext("2d");
1663
+ if (!ctx) {
1664
+ reject(new Error("canvas-context-failed"));
1596
1665
  return;
1597
1666
  }
1598
- const img = new Image();
1599
- img.onload = () => {
1600
- const size = 64;
1601
- const canvas = document.createElement("canvas");
1602
- canvas.width = size;
1603
- canvas.height = size;
1604
- const ctx = canvas.getContext("2d");
1605
- if (!ctx) {
1606
- reject(new Error("canvas-context-failed"));
1607
- return;
1608
- }
1609
- ctx.clearRect(0, 0, size, size);
1610
- // 保持长宽比居中绘制,统一导出 PNG,提升 favicon 兼容性
1611
- const scale = Math.min(size / img.width, size / img.height);
1612
- const drawW = img.width * scale;
1613
- const drawH = img.height * scale;
1614
- const dx = (size - drawW) / 2;
1615
- const dy = (size - drawH) / 2;
1616
- ctx.drawImage(img, dx, dy, drawW, drawH);
1617
- resolve(canvas.toDataURL("image/png"));
1618
- };
1619
- img.onerror = () => reject(new Error("image-decode-failed"));
1620
- img.src = src;
1667
+ ctx.clearRect(0, 0, size, size);
1668
+ const scale = Math.min(size / img.width, size / img.height);
1669
+ const drawW = img.width * scale;
1670
+ const drawH = img.height * scale;
1671
+ const dx = (size - drawW) / 2;
1672
+ const dy = (size - drawH) / 2;
1673
+ ctx.drawImage(img, dx, dy, drawW, drawH);
1674
+ img.src = "";
1675
+ resolve(canvas.toDataURL("image/png"));
1621
1676
  };
1622
- reader.readAsDataURL(file);
1677
+ img.onerror = () => reject(new Error("image-decode-failed"));
1678
+ img.src = src;
1623
1679
  });
1624
1680
  }, []);
1625
1681
 
@@ -1784,16 +1840,49 @@ export function ReactViewPanel({
1784
1840
  });
1785
1841
  }
1786
1842
  var scene = document.getElementById("preview-scene");
1843
+ var root = document.getElementById("preview-root");
1844
+ var outputScale = ${outputScale ? "true" : "false"};
1845
+ function rememberBox(node) {
1846
+ if (node.dataset.origLeft != null) return;
1847
+ node.dataset.origLeft = String(parseFloat(node.style.left) || 0);
1848
+ node.dataset.origTop = String(parseFloat(node.style.top) || 0);
1849
+ node.dataset.origWidth = String(parseFloat(node.style.width) || node.offsetWidth || 0);
1850
+ node.dataset.origHeight = String(parseFloat(node.style.height) || node.offsetHeight || 0);
1851
+ }
1787
1852
  function fitScene() {
1788
1853
  if (!scene) return;
1789
1854
  var vw = window.innerWidth || 1;
1790
1855
  var vh = window.innerHeight || 1;
1791
1856
  var sw = ${sceneWidth} || 1;
1792
1857
  var sh = ${sceneHeight} || 1;
1793
- // 铺满预览窗口:宽高分别缩放,保证内容贴住四边
1794
1858
  var scaleX = vw / sw;
1795
1859
  var scaleY = vh / sh;
1796
- scene.style.transform = "translate(0px,0px) scale(" + scaleX + "," + scaleY + ")";
1860
+ if (root) root.style.overflow = "hidden";
1861
+ var nodes = scene.querySelectorAll("[data-element-id]");
1862
+ if (outputScale) {
1863
+ scene.style.width = sw + "px";
1864
+ scene.style.height = sh + "px";
1865
+ scene.style.transform = "translate(0px,0px) scale(" + scaleX + "," + scaleY + ")";
1866
+ nodes.forEach(function (node) {
1867
+ rememberBox(node);
1868
+ node.style.left = node.dataset.origLeft + "px";
1869
+ node.style.top = node.dataset.origTop + "px";
1870
+ node.style.width = node.dataset.origWidth + "px";
1871
+ node.style.height = node.dataset.origHeight + "px";
1872
+ });
1873
+ return;
1874
+ }
1875
+ // 块仍按视口撑满,但不对整棵内容做 transform,字体保持原尺寸
1876
+ scene.style.width = vw + "px";
1877
+ scene.style.height = vh + "px";
1878
+ scene.style.transform = "none";
1879
+ nodes.forEach(function (node) {
1880
+ rememberBox(node);
1881
+ node.style.left = (Number(node.dataset.origLeft) * scaleX) + "px";
1882
+ node.style.top = (Number(node.dataset.origTop) * scaleY) + "px";
1883
+ node.style.width = (Number(node.dataset.origWidth) * scaleX) + "px";
1884
+ node.style.height = (Number(node.dataset.origHeight) * scaleY) + "px";
1885
+ });
1797
1886
  }
1798
1887
  fitScene();
1799
1888
  window.addEventListener("resize", fitScene);
@@ -1895,6 +1984,7 @@ export function ReactViewPanel({
1895
1984
  ].join(" ")}
1896
1985
  style={{ ["--panel-font-px" as string]: `${panelFontPx}px` }}
1897
1986
  >
1987
+ <BusyOverlay />
1898
1988
  <TooltipProvider delayDuration={120}>
1899
1989
  <Tooltip open={ellipsisTooltip.open}>
1900
1990
  <TooltipTrigger asChild>
@@ -1913,6 +2003,11 @@ export function ReactViewPanel({
1913
2003
  .panel-font-root :is(span, label, p, strong, button, input, textarea, select, option, a, li) {
1914
2004
  font-size: var(--panel-font-px) !important;
1915
2005
  }
2006
+ /* 画布节点、富文本内容使用各自的字号,不被编辑器 UI 字号强制覆盖 */
2007
+ .panel-font-root [data-workspace-region="view"] :is(span, label, p, strong, button, input, textarea, select, option, a, li),
2008
+ .panel-font-root [data-panel-user-text] :is(span, label, p, strong, button, input, textarea, select, option, a, li) {
2009
+ font-size: inherit !important;
2010
+ }
1916
2011
  .panel-font-root :is(input, textarea):focus,
1917
2012
  .panel-font-root :is(input, textarea):focus-visible {
1918
2013
  outline: none !important;
@@ -2013,6 +2108,14 @@ export function ReactViewPanel({
2013
2108
  <MenubarRadioItem value="zh-CN">{t("panel.theme.zhCN")}</MenubarRadioItem>
2014
2109
  <MenubarRadioItem value="en-US">{t("panel.theme.enUS")}</MenubarRadioItem>
2015
2110
  </MenubarRadioGroup>
2111
+ <MenubarSeparator />
2112
+ <MenubarCheckboxItem
2113
+ checked={outputScale}
2114
+ onCheckedChange={(checked) => setOutputScale(checked === true)}
2115
+ title={t("panel.menubar.outputScaleHint")}
2116
+ >
2117
+ {t("panel.menubar.outputScale")}
2118
+ </MenubarCheckboxItem>
2016
2119
  </MenubarContent>
2017
2120
  </MenubarMenu>
2018
2121
  </Menubar>
@@ -2021,6 +2124,7 @@ export function ReactViewPanel({
2021
2124
  activeProjectId={workspaceProjects.activeProjectId}
2022
2125
  activeProjectName={workspaceProjects.activeProjectName}
2023
2126
  dirty={workspaceProjects.dirty}
2127
+ previewingProjectIds={workspaceProjects.previewingProjectIds}
2024
2128
  onCreateProject={handleWorkspaceCreateProject}
2025
2129
  onOpenProject={handleWorkspaceOpenProject}
2026
2130
  onSyncProject={handleWorkspaceSyncProject}
@@ -2510,8 +2614,10 @@ export function ReactViewPanel({
2510
2614
  try {
2511
2615
  const normalizedDataUrl = await normalizeTitleIconFile(file);
2512
2616
  setTitleIconDataUrl(normalizedDataUrl);
2513
- } catch {
2514
- window.alert(messages.iconReadFailed);
2617
+ } catch (error) {
2618
+ window.alert(
2619
+ error instanceof Error ? error.message : messages.iconReadFailed
2620
+ );
2515
2621
  }
2516
2622
  }}
2517
2623
  />
@@ -2644,6 +2750,10 @@ export function ReactViewPanel({
2644
2750
  onTableCellAction={(payload) => {
2645
2751
  void triggerBlueprintNode(payload.blueprintNodeId, payload);
2646
2752
  }}
2753
+ boundViewEventTypes={boundViewEventTypes}
2754
+ onViewUiEvent={(payload) => {
2755
+ void emitViewEvent(payload);
2756
+ }}
2647
2757
  />
2648
2758
 
2649
2759
  <SelectLayer
@@ -3176,6 +3286,8 @@ export function ReactViewPanel({
3176
3286
  configFocus={sidebarConfigFocus}
3177
3287
  executionLog={executionLogView}
3178
3288
  selectedBlueprintNode={selectedBlueprintNode}
3289
+ blueprintGraphNodes={blueprintGraph.document.nodes}
3290
+ blueprintGraphEdges={blueprintGraph.document.edges}
3179
3291
  allowFalseSignalPropagation={
3180
3292
  blueprintGraph.document.allowFalseSignalPropagation ?? false
3181
3293
  }
@@ -202,6 +202,7 @@ function applyBlueprintAddNode(
202
202
  fetch: "fetch",
203
203
  json: "json",
204
204
  and: "and",
205
+ event: "event",
205
206
  };
206
207
  const configSource = configSourceMap[alias];
207
208
  if (configSource && configSource !== "logic") {
@@ -0,0 +1,25 @@
1
+ import { createPortal } from "react-dom";
2
+ import { useI18n } from "@arronqzy/i18n/react";
3
+ import { useBusyOverlayMessage } from "../utils/async-work";
4
+
5
+ export function BusyOverlay() {
6
+ const { t } = useI18n();
7
+ const message = useBusyOverlayMessage();
8
+ if (!message || typeof document === "undefined") return null;
9
+
10
+ return createPortal(
11
+ <div
12
+ className="fixed inset-0 flex flex-col items-center justify-center gap-3 bg-black/45 px-6"
13
+ style={{ zIndex: 40000 }}
14
+ role="status"
15
+ aria-live="polite"
16
+ aria-busy="true"
17
+ >
18
+ <div className="h-9 w-9 animate-spin rounded-full border-2 border-white/30 border-t-white" />
19
+ <div className="max-w-sm text-center text-sm text-white">
20
+ {message || t("common.loading")}
21
+ </div>
22
+ </div>,
23
+ document.body
24
+ );
25
+ }