@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
|
@@ -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
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
|
@@ -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
|
+
}
|