@arronqzy/react-view 1.0.21 → 1.0.23
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/package.json +11 -11
- package/readme.md +6 -1
- package/src/panel/I18nRoot.tsx +14 -2
- package/src/panel/ReactViewOnlinePreview.tsx +29 -18
- package/src/panel/ReactViewPanel.tsx +154 -1006
- package/src/panel/components/PanelConfigSidebar.tsx +297 -3712
- package/src/panel/components/PanelHistoryPopover.tsx +163 -0
- package/src/panel/components/PanelLayerDock.tsx +412 -0
- package/src/panel/components/PanelMenubar.tsx +149 -0
- package/src/panel/components/PanelWorkspaceDialogs.tsx +271 -0
- package/src/panel/components/config/PanelConfigAudioSection.tsx +334 -0
- package/src/panel/components/config/PanelConfigChartSection.tsx +799 -0
- package/src/panel/components/config/PanelConfigGeometrySection.tsx +233 -0
- package/src/panel/components/config/PanelConfigGridChildSpan.tsx +70 -0
- package/src/panel/components/config/PanelConfigGridSection.tsx +118 -0
- package/src/panel/components/config/PanelConfigMultiSelect.tsx +1291 -0
- package/src/panel/components/config/PanelConfigNodeInfo.tsx +169 -0
- package/src/panel/components/config/PanelConfigReferenceSection.tsx +99 -0
- package/src/panel/components/config/PanelConfigScene3dSection.tsx +6 -2
- package/src/panel/components/config/PanelConfigStyleSections.tsx +249 -0
- package/src/panel/components/config/PanelConfigTextSection.tsx +268 -0
- package/src/panel/components/config/PanelConfigVideoSection.tsx +133 -0
- package/src/panel/components/config/PanelConfigViewportSection.tsx +139 -0
- package/src/panel/components/config/helpers.ts +85 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +112 -34
- package/src/panel/icons.tsx +160 -0
- package/src/panel/index.tsx +15 -1
- package/src/panel/library/parse-workspace-data.ts +142 -0
- package/src/panel/library/workspace-project-cache.ts +13 -4
- package/src/panel/library/workspace-project-db.ts +24 -13
- package/src/panel/library/workspace-project-sync.ts +17 -6
- package/src/panel/utils/outputScale.ts +9 -4
- package/src/panel/utils/panelSelection.ts +57 -0
- package/src/panel/utils/panelStateIO.ts +18 -0
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
} from "../library/workspace-snapshot";
|
|
22
22
|
import { runBusyTask } from "../utils/async-work";
|
|
23
23
|
import { AbuilderEvents, emitAbuilderEvent } from "../library/event-subscription";
|
|
24
|
+
import { parseBlueprintData } from "@arronqzy/react-blueprint";
|
|
25
|
+
import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
|
|
24
26
|
|
|
25
27
|
export type UseWorkspaceProjectsOptions = {
|
|
26
28
|
exportPanelData: () => State;
|
|
@@ -39,13 +41,20 @@ export type UseWorkspaceProjectsOptions = {
|
|
|
39
41
|
onProjectApplied?: (record: WorkspaceProjectRecord) => void;
|
|
40
42
|
/** 外部传入的初始工作区,挂载后自动加载(不触发 onProjectApplied) */
|
|
41
43
|
initialWorkspace?: WorkspaceProjectRecord | null;
|
|
44
|
+
/** 隔离 IndexedDB / 预览缓存 / 同步频道 */
|
|
45
|
+
nameSpace?: string | null;
|
|
42
46
|
};
|
|
43
47
|
|
|
44
|
-
export function buildOnlinePreviewUrl(
|
|
48
|
+
export function buildOnlinePreviewUrl(
|
|
49
|
+
projectId: string,
|
|
50
|
+
nameSpace?: string | null
|
|
51
|
+
): string {
|
|
45
52
|
const url = new URL(window.location.href);
|
|
46
53
|
url.search = "";
|
|
47
54
|
url.searchParams.set("preview", "online");
|
|
48
55
|
url.searchParams.set("projectId", projectId);
|
|
56
|
+
const ns = (nameSpace ?? "").trim();
|
|
57
|
+
if (ns) url.searchParams.set("ns", ns);
|
|
49
58
|
const previewInstanceId =
|
|
50
59
|
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
51
60
|
? crypto.randomUUID()
|
|
@@ -54,6 +63,28 @@ export function buildOnlinePreviewUrl(projectId: string): string {
|
|
|
54
63
|
return url.toString();
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
function previewWindowName(projectId: string, nameSpace?: string | null): string {
|
|
67
|
+
const ns = (nameSpace ?? "").trim();
|
|
68
|
+
return ns ? `abuilder-preview-${ns}-${projectId}` : `abuilder-preview-${projectId}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasWorkspacePayload(
|
|
72
|
+
record?: WorkspaceProjectRecord | null
|
|
73
|
+
): record is WorkspaceProjectRecord {
|
|
74
|
+
if (!record || typeof record !== "object") return false;
|
|
75
|
+
return Boolean(
|
|
76
|
+
record.panelState ||
|
|
77
|
+
record.blueprintDocument ||
|
|
78
|
+
record.id ||
|
|
79
|
+
record.productName ||
|
|
80
|
+
record.titleIconDataUrl
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function workspaceApplyKey(record: WorkspaceProjectRecord): string {
|
|
85
|
+
return `${record.id ?? "anon"}:${record.updatedAt ?? 0}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
57
88
|
export function useWorkspaceProjects({
|
|
58
89
|
exportPanelData,
|
|
59
90
|
importPanelData,
|
|
@@ -68,6 +99,7 @@ export function useWorkspaceProjects({
|
|
|
68
99
|
panelRevision,
|
|
69
100
|
onProjectApplied,
|
|
70
101
|
initialWorkspace,
|
|
102
|
+
nameSpace = null,
|
|
71
103
|
}: UseWorkspaceProjectsOptions) {
|
|
72
104
|
const { t } = useI18nOptional();
|
|
73
105
|
const [projects, setProjects] = useState<WorkspaceProjectListItem[]>([]);
|
|
@@ -99,7 +131,8 @@ export function useWorkspaceProjects({
|
|
|
99
131
|
}, [refreshPreviewing]);
|
|
100
132
|
|
|
101
133
|
const resolveProjectName = useCallback(() => {
|
|
102
|
-
return productName.trim() ||
|
|
134
|
+
return (typeof productName === "string" ? productName : "").trim() ||
|
|
135
|
+
t("panel.defaults.unnamedProduct");
|
|
103
136
|
}, [productName, t]);
|
|
104
137
|
|
|
105
138
|
const buildCurrentSnapshot = useCallback((): WorkspaceSnapshot => {
|
|
@@ -112,9 +145,9 @@ export function useWorkspaceProjects({
|
|
|
112
145
|
}, [blueprintDocument, blueprintMeta, productName, titleIconDataUrl]);
|
|
113
146
|
|
|
114
147
|
const refreshProjects = useCallback(async () => {
|
|
115
|
-
const items = await listWorkspaceProjects();
|
|
148
|
+
const items = await listWorkspaceProjects(nameSpace);
|
|
116
149
|
setProjects(items);
|
|
117
|
-
}, []);
|
|
150
|
+
}, [nameSpace]);
|
|
118
151
|
|
|
119
152
|
useEffect(() => {
|
|
120
153
|
void refreshProjects();
|
|
@@ -143,25 +176,33 @@ export function useWorkspaceProjects({
|
|
|
143
176
|
|
|
144
177
|
const applyProjectRecord = useCallback(
|
|
145
178
|
(record: WorkspaceProjectRecord, options?: { notifyApplied?: boolean }) => {
|
|
179
|
+
const parsedBlueprint = parseBlueprintData(record.blueprintDocument);
|
|
146
180
|
const blueprintDocument =
|
|
147
|
-
|
|
181
|
+
parsedBlueprint.value ?? BlueprintGraph.empty().document;
|
|
148
182
|
const blueprintMeta: BlueprintMetaDraft = {
|
|
149
183
|
name: record.blueprintMeta?.name ?? t("panel.defaults.unnamedBlueprint"),
|
|
150
184
|
remark: record.blueprintMeta?.remark ?? "",
|
|
151
185
|
};
|
|
186
|
+
const panelState =
|
|
187
|
+
normalizeImportedPanelState(record.panelState) ?? createEmptyPanelState();
|
|
188
|
+
const productName =
|
|
189
|
+
typeof record.productName === "string" && record.productName.trim()
|
|
190
|
+
? record.productName
|
|
191
|
+
: t("panel.defaults.unnamedProduct");
|
|
192
|
+
const titleIconDataUrl = record.titleIconDataUrl ?? "";
|
|
152
193
|
|
|
153
|
-
importPanelData(
|
|
194
|
+
importPanelData(panelState);
|
|
154
195
|
setBlueprintGraph(BlueprintGraph.fromDocument(blueprintDocument));
|
|
155
196
|
setBlueprintMeta(blueprintMeta);
|
|
156
|
-
setProductName(
|
|
157
|
-
setTitleIconDataUrl(
|
|
158
|
-
setActiveProjectId(record.id);
|
|
159
|
-
setActiveProjectName(record.name);
|
|
197
|
+
setProductName(productName);
|
|
198
|
+
setTitleIconDataUrl(titleIconDataUrl);
|
|
199
|
+
setActiveProjectId(record.id || null);
|
|
200
|
+
setActiveProjectName(record.name ?? null);
|
|
160
201
|
syncedSnapshotRef.current = cloneWorkspaceSnapshot({
|
|
161
202
|
blueprintDocument,
|
|
162
203
|
blueprintMeta,
|
|
163
|
-
productName
|
|
164
|
-
titleIconDataUrl
|
|
204
|
+
productName,
|
|
205
|
+
titleIconDataUrl,
|
|
165
206
|
});
|
|
166
207
|
syncedPanelRevisionRef.current = panelRevision;
|
|
167
208
|
pendingRevisionSyncRef.current = true;
|
|
@@ -170,6 +211,9 @@ export function useWorkspaceProjects({
|
|
|
170
211
|
...record,
|
|
171
212
|
blueprintDocument,
|
|
172
213
|
blueprintMeta,
|
|
214
|
+
panelState,
|
|
215
|
+
productName,
|
|
216
|
+
titleIconDataUrl: titleIconDataUrl || undefined,
|
|
173
217
|
});
|
|
174
218
|
}
|
|
175
219
|
},
|
|
@@ -185,14 +229,46 @@ export function useWorkspaceProjects({
|
|
|
185
229
|
]
|
|
186
230
|
);
|
|
187
231
|
|
|
232
|
+
const applyEmptyWorkspace = useCallback(() => {
|
|
233
|
+
importPanelData(createEmptyPanelState());
|
|
234
|
+
setBlueprintGraph(BlueprintGraph.empty());
|
|
235
|
+
setBlueprintMeta({
|
|
236
|
+
name: t("panel.defaults.unnamedBlueprint"),
|
|
237
|
+
remark: "",
|
|
238
|
+
});
|
|
239
|
+
setProductName(t("panel.defaults.unnamedProduct"));
|
|
240
|
+
setTitleIconDataUrl("");
|
|
241
|
+
setActiveProjectId(null);
|
|
242
|
+
setActiveProjectName(null);
|
|
243
|
+
syncedSnapshotRef.current = null;
|
|
244
|
+
}, [
|
|
245
|
+
importPanelData,
|
|
246
|
+
setBlueprintGraph,
|
|
247
|
+
setBlueprintMeta,
|
|
248
|
+
setProductName,
|
|
249
|
+
setTitleIconDataUrl,
|
|
250
|
+
t,
|
|
251
|
+
]);
|
|
252
|
+
|
|
188
253
|
const initialWorkspaceAppliedKeyRef = useRef<string | null>(null);
|
|
254
|
+
const bootstrappedRef = useRef(false);
|
|
189
255
|
useEffect(() => {
|
|
190
|
-
if (!
|
|
191
|
-
|
|
256
|
+
if (!bootstrappedRef.current) {
|
|
257
|
+
bootstrappedRef.current = true;
|
|
258
|
+
if (hasWorkspacePayload(initialWorkspace)) {
|
|
259
|
+
applyProjectRecord(initialWorkspace, { notifyApplied: false });
|
|
260
|
+
initialWorkspaceAppliedKeyRef.current = workspaceApplyKey(initialWorkspace);
|
|
261
|
+
} else {
|
|
262
|
+
applyEmptyWorkspace();
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!hasWorkspacePayload(initialWorkspace)) return;
|
|
267
|
+
const key = workspaceApplyKey(initialWorkspace);
|
|
192
268
|
if (initialWorkspaceAppliedKeyRef.current === key) return;
|
|
193
269
|
initialWorkspaceAppliedKeyRef.current = key;
|
|
194
270
|
applyProjectRecord(initialWorkspace, { notifyApplied: false });
|
|
195
|
-
}, [applyProjectRecord, initialWorkspace]);
|
|
271
|
+
}, [applyEmptyWorkspace, applyProjectRecord, initialWorkspace]);
|
|
196
272
|
|
|
197
273
|
const persistProject = useCallback(
|
|
198
274
|
async (options: { id?: string; name: string; createdAt?: number }) => {
|
|
@@ -200,7 +276,7 @@ export function useWorkspaceProjects({
|
|
|
200
276
|
const now = Date.now();
|
|
201
277
|
const id = options.id ?? createWorkspaceProjectId();
|
|
202
278
|
const createdAt = options.createdAt ?? now;
|
|
203
|
-
const name = options.name.trim() || resolveProjectName();
|
|
279
|
+
const name = (options.name ?? "").trim() || resolveProjectName();
|
|
204
280
|
|
|
205
281
|
const record: WorkspaceProjectRecord = {
|
|
206
282
|
id,
|
|
@@ -210,13 +286,14 @@ export function useWorkspaceProjects({
|
|
|
210
286
|
panelState: exportPanelData(),
|
|
211
287
|
blueprintDocument: snapshot.blueprintDocument,
|
|
212
288
|
blueprintMeta: snapshot.blueprintMeta,
|
|
213
|
-
productName:
|
|
289
|
+
productName:
|
|
290
|
+
typeof snapshot.productName === "string" ? snapshot.productName : resolveProjectName(),
|
|
214
291
|
titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
|
|
215
292
|
};
|
|
216
293
|
|
|
217
|
-
await putWorkspaceProject(record);
|
|
218
|
-
writeWorkspacePreviewCache(record);
|
|
219
|
-
broadcastWorkspaceProjectUpdate(id, now);
|
|
294
|
+
await putWorkspaceProject(record, nameSpace);
|
|
295
|
+
writeWorkspacePreviewCache(record, nameSpace);
|
|
296
|
+
broadcastWorkspaceProjectUpdate(id, now, nameSpace);
|
|
220
297
|
setActiveProjectId(id);
|
|
221
298
|
setActiveProjectName(name);
|
|
222
299
|
syncedSnapshotRef.current = snapshot;
|
|
@@ -225,7 +302,7 @@ export function useWorkspaceProjects({
|
|
|
225
302
|
await refreshProjects();
|
|
226
303
|
return record;
|
|
227
304
|
},
|
|
228
|
-
[buildCurrentSnapshot, exportPanelData, panelRevision, resolveProjectName, refreshProjects]
|
|
305
|
+
[buildCurrentSnapshot, exportPanelData, nameSpace, panelRevision, resolveProjectName, refreshProjects]
|
|
229
306
|
);
|
|
230
307
|
|
|
231
308
|
const handleCreateProject = useCallback(async () => {
|
|
@@ -242,7 +319,7 @@ export function useWorkspaceProjects({
|
|
|
242
319
|
return handleCreateProject();
|
|
243
320
|
}
|
|
244
321
|
return runBusyTask(t("common.savingWorkspace"), async () => {
|
|
245
|
-
const existing = await getWorkspaceProject(activeProjectId);
|
|
322
|
+
const existing = await getWorkspaceProject(activeProjectId, nameSpace);
|
|
246
323
|
const name = resolveProjectName();
|
|
247
324
|
const record = await persistProject({
|
|
248
325
|
id: activeProjectId,
|
|
@@ -251,12 +328,12 @@ export function useWorkspaceProjects({
|
|
|
251
328
|
});
|
|
252
329
|
return { name, id: record.id };
|
|
253
330
|
});
|
|
254
|
-
}, [activeProjectId, handleCreateProject, persistProject, resolveProjectName, t]);
|
|
331
|
+
}, [activeProjectId, handleCreateProject, nameSpace, persistProject, resolveProjectName, t]);
|
|
255
332
|
|
|
256
333
|
const handleOpenProject = useCallback(
|
|
257
334
|
async (id: string) => {
|
|
258
335
|
await runBusyTask(t("common.loadingWorkspace"), async () => {
|
|
259
|
-
const record = await getWorkspaceProject(id);
|
|
336
|
+
const record = await getWorkspaceProject(id, nameSpace);
|
|
260
337
|
if (!record) {
|
|
261
338
|
await refreshProjects();
|
|
262
339
|
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
@@ -264,7 +341,7 @@ export function useWorkspaceProjects({
|
|
|
264
341
|
applyProjectRecord(record);
|
|
265
342
|
});
|
|
266
343
|
},
|
|
267
|
-
[applyProjectRecord, refreshProjects, t]
|
|
344
|
+
[applyProjectRecord, nameSpace, refreshProjects, t]
|
|
268
345
|
);
|
|
269
346
|
|
|
270
347
|
const handleSyncProject = useCallback(async () => {
|
|
@@ -272,7 +349,7 @@ export function useWorkspaceProjects({
|
|
|
272
349
|
if (!activeProjectId) {
|
|
273
350
|
throw new Error(t("panel.messages.saveWorkspaceFirst"));
|
|
274
351
|
}
|
|
275
|
-
const existing = await getWorkspaceProject(activeProjectId);
|
|
352
|
+
const existing = await getWorkspaceProject(activeProjectId, nameSpace);
|
|
276
353
|
if (!existing) {
|
|
277
354
|
setActiveProjectId(null);
|
|
278
355
|
setActiveProjectName(null);
|
|
@@ -289,12 +366,12 @@ export function useWorkspaceProjects({
|
|
|
289
366
|
emitAbuilderEvent(AbuilderEvents.workspaceSync, record);
|
|
290
367
|
return name;
|
|
291
368
|
});
|
|
292
|
-
}, [activeProjectId, persistProject, refreshProjects, resolveProjectName, t]);
|
|
369
|
+
}, [activeProjectId, nameSpace, persistProject, refreshProjects, resolveProjectName, t]);
|
|
293
370
|
|
|
294
371
|
const handleDeleteProject = useCallback(
|
|
295
372
|
async (id: string) => {
|
|
296
373
|
await runBusyTask(t("common.deletingWorkspace"), async () => {
|
|
297
|
-
await deleteWorkspaceProject(id);
|
|
374
|
+
await deleteWorkspaceProject(id, nameSpace);
|
|
298
375
|
if (activeProjectId === id) {
|
|
299
376
|
setActiveProjectId(null);
|
|
300
377
|
setActiveProjectName(null);
|
|
@@ -305,7 +382,7 @@ export function useWorkspaceProjects({
|
|
|
305
382
|
await refreshProjects();
|
|
306
383
|
});
|
|
307
384
|
},
|
|
308
|
-
[activeProjectId, refreshPreviewing, refreshProjects, t]
|
|
385
|
+
[activeProjectId, nameSpace, refreshPreviewing, refreshProjects, t]
|
|
309
386
|
);
|
|
310
387
|
|
|
311
388
|
const handlePreviewProject = useCallback(
|
|
@@ -315,9 +392,10 @@ export function useWorkspaceProjects({
|
|
|
315
392
|
await handleSyncProject();
|
|
316
393
|
}
|
|
317
394
|
const record =
|
|
318
|
-
(await getWorkspaceProject(id)) ??
|
|
395
|
+
(await getWorkspaceProject(id, nameSpace)) ??
|
|
396
|
+
readWorkspacePreviewCache(id, nameSpace);
|
|
319
397
|
if (record) {
|
|
320
|
-
writeWorkspacePreviewCache(record);
|
|
398
|
+
writeWorkspacePreviewCache(record, nameSpace);
|
|
321
399
|
}
|
|
322
400
|
const existing = previewWindowsRef.current.get(id);
|
|
323
401
|
if (existing && !existing.closed) {
|
|
@@ -330,8 +408,8 @@ export function useWorkspaceProjects({
|
|
|
330
408
|
return;
|
|
331
409
|
}
|
|
332
410
|
const win = window.open(
|
|
333
|
-
buildOnlinePreviewUrl(id),
|
|
334
|
-
|
|
411
|
+
buildOnlinePreviewUrl(id, nameSpace),
|
|
412
|
+
previewWindowName(id, nameSpace)
|
|
335
413
|
);
|
|
336
414
|
if (win) {
|
|
337
415
|
previewWindowsRef.current.set(id, win);
|
|
@@ -345,7 +423,7 @@ export function useWorkspaceProjects({
|
|
|
345
423
|
refreshPreviewing();
|
|
346
424
|
});
|
|
347
425
|
},
|
|
348
|
-
[activeProjectId, dirty, handleSyncProject, refreshPreviewing, t]
|
|
426
|
+
[activeProjectId, dirty, handleSyncProject, nameSpace, refreshPreviewing, t]
|
|
349
427
|
);
|
|
350
428
|
|
|
351
429
|
return {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export function IconPlus() {
|
|
4
|
+
return (
|
|
5
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
6
|
+
<path d="M12 5v14M5 12h14" />
|
|
7
|
+
</svg>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function IconMerge() {
|
|
12
|
+
return (
|
|
13
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
14
|
+
<path d="M7 7h4a4 4 0 0 1 4 4v6" />
|
|
15
|
+
<path d="m12 14 3 3 3-3" />
|
|
16
|
+
<path d="M7 17h2" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function IconLock({ locked }: { locked: boolean }) {
|
|
22
|
+
return locked ? (
|
|
23
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
24
|
+
<rect x="4" y="11" width="16" height="9" rx="2" />
|
|
25
|
+
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
|
|
26
|
+
</svg>
|
|
27
|
+
) : (
|
|
28
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
29
|
+
<rect x="4" y="11" width="16" height="9" rx="2" />
|
|
30
|
+
<path d="M16 11V8a4 4 0 0 0-7-2.5" />
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function IconEdit() {
|
|
36
|
+
return (
|
|
37
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
38
|
+
<path d="M12 20h9" />
|
|
39
|
+
<path d="m16.5 3.5 4 4L8 20l-5 1 1-5Z" />
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function IconTrash() {
|
|
45
|
+
return (
|
|
46
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
47
|
+
<path d="M3 6h18" />
|
|
48
|
+
<path d="M8 6V4h8v2" />
|
|
49
|
+
<path d="M19 6l-1 14H6L5 6" />
|
|
50
|
+
</svg>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function IconCheck() {
|
|
55
|
+
return (
|
|
56
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
57
|
+
<path d="m5 12 5 5L20 7" />
|
|
58
|
+
</svg>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function IconClose() {
|
|
63
|
+
return (
|
|
64
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
65
|
+
<path d="M6 6l12 12M18 6 6 18" />
|
|
66
|
+
</svg>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function IconChevron({ expanded }: { expanded: boolean }) {
|
|
71
|
+
return (
|
|
72
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
73
|
+
{expanded ? <path d="m6 14 6-6 6 6" /> : <path d="m6 10 6 6 6-6" />}
|
|
74
|
+
</svg>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function IconUndo() {
|
|
79
|
+
return (
|
|
80
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
81
|
+
<path d="M9 14 4 9l5-5" />
|
|
82
|
+
<path d="M20 20a9 9 0 0 0-9-9H4" />
|
|
83
|
+
</svg>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function IconRedo() {
|
|
88
|
+
return (
|
|
89
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
90
|
+
<path d="m15 14 5-5-5-5" />
|
|
91
|
+
<path d="M4 20a9 9 0 0 1 9-9h7" />
|
|
92
|
+
</svg>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function IconHistory() {
|
|
97
|
+
return (
|
|
98
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
99
|
+
<path d="M3 12a9 9 0 1 0 3-6.7" />
|
|
100
|
+
<path d="M3 4v4h4" />
|
|
101
|
+
<path d="M12 7v5l3 2" />
|
|
102
|
+
</svg>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function IconBringFront() {
|
|
107
|
+
return (
|
|
108
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
109
|
+
<path d="M7 17h10M12 17V7" />
|
|
110
|
+
<path d="m8.5 9 3.5-3.5L15.5 9" />
|
|
111
|
+
</svg>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function IconSendBack() {
|
|
116
|
+
return (
|
|
117
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
118
|
+
<path d="M7 7h10M12 17V7" />
|
|
119
|
+
<path d="m15.5 15-3.5 3.5L8.5 15" />
|
|
120
|
+
</svg>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function IconForward() {
|
|
125
|
+
return (
|
|
126
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
127
|
+
<path d="M12 18V6" />
|
|
128
|
+
<path d="m8.5 9 3.5-3.5L15.5 9" />
|
|
129
|
+
</svg>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function IconBackward() {
|
|
134
|
+
return (
|
|
135
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
136
|
+
<path d="M12 6v12" />
|
|
137
|
+
<path d="m15.5 15-3.5 3.5L8.5 15" />
|
|
138
|
+
</svg>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function IconLayers() {
|
|
143
|
+
return (
|
|
144
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
145
|
+
<path d="m12 4 8 4-8 4-8-4 8-4Z" />
|
|
146
|
+
<path d="m4 12 8 4 8-4" />
|
|
147
|
+
<path d="m4 16 8 4 8-4" />
|
|
148
|
+
</svg>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function IconImage() {
|
|
153
|
+
return (
|
|
154
|
+
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
|
|
155
|
+
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
156
|
+
<circle cx="9" cy="10" r="1.5" />
|
|
157
|
+
<path d="m21 16-5.5-5.5L7 19" />
|
|
158
|
+
</svg>
|
|
159
|
+
);
|
|
160
|
+
}
|
package/src/panel/index.tsx
CHANGED
|
@@ -2,4 +2,18 @@ export * from "./ReactViewPanel";
|
|
|
2
2
|
export * from "./ReactViewOnlinePreview";
|
|
3
3
|
export { I18nRoot, I18nProvider } from "./I18nRoot";
|
|
4
4
|
export type { I18nRootProps, Locale } from "./I18nRoot";
|
|
5
|
-
export type * from "./types";
|
|
5
|
+
export type * from "./types";
|
|
6
|
+
export {
|
|
7
|
+
parseViewData,
|
|
8
|
+
validateViewData,
|
|
9
|
+
parseBlueprintData,
|
|
10
|
+
validateBlueprintData,
|
|
11
|
+
validateWorkspaceData,
|
|
12
|
+
parseWorkspaceData,
|
|
13
|
+
createEmptyWorkspace,
|
|
14
|
+
} from "./library/parse-workspace-data";
|
|
15
|
+
export { createWorkspaceProjectId } from "./library/workspace-project-db";
|
|
16
|
+
export type {
|
|
17
|
+
ParseCheckResult,
|
|
18
|
+
WorkspaceParseCheckResult,
|
|
19
|
+
} from "./library/parse-workspace-data";
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlueprintGraph,
|
|
3
|
+
parseBlueprintData,
|
|
4
|
+
validateBlueprintData,
|
|
5
|
+
type BlueprintDocument,
|
|
6
|
+
type BlueprintMetaDraft,
|
|
7
|
+
type ParseCheckResult,
|
|
8
|
+
} from "@arronqzy/react-blueprint";
|
|
9
|
+
import type { State } from "@arronqzy/rx-store";
|
|
10
|
+
import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
|
|
11
|
+
import {
|
|
12
|
+
createWorkspaceProjectId,
|
|
13
|
+
type WorkspaceProjectRecord,
|
|
14
|
+
} from "./workspace-project-db";
|
|
15
|
+
|
|
16
|
+
export type { ParseCheckResult };
|
|
17
|
+
|
|
18
|
+
export type WorkspaceParseCheckResult = ParseCheckResult & {
|
|
19
|
+
view: ParseCheckResult;
|
|
20
|
+
blueprint: ParseCheckResult;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
24
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractPanelState(data: unknown): unknown {
|
|
28
|
+
if (isRecord(data) && data.panelState !== undefined) return data.panelState;
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readBlueprintMeta(raw: unknown): BlueprintMetaDraft {
|
|
33
|
+
if (!isRecord(raw)) return { name: "", remark: "" };
|
|
34
|
+
return {
|
|
35
|
+
name: typeof raw.name === "string" ? raw.name : "",
|
|
36
|
+
remark: typeof raw.remark === "string" ? raw.remark : "",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 校验数据能否被视图解析(工作区 `panelState` 或裸面板状态均可)。
|
|
42
|
+
*/
|
|
43
|
+
export function parseViewData(
|
|
44
|
+
data: unknown
|
|
45
|
+
): ParseCheckResult & { value?: State } {
|
|
46
|
+
if (data == null) {
|
|
47
|
+
return { ok: false, error: "empty" };
|
|
48
|
+
}
|
|
49
|
+
const normalized = normalizeImportedPanelState(extractPanelState(data));
|
|
50
|
+
if (!normalized) {
|
|
51
|
+
return { ok: false, error: "invalid-view-data" };
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, value: normalized };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function validateViewData(data: unknown): ParseCheckResult {
|
|
57
|
+
const parsed = parseViewData(data);
|
|
58
|
+
return parsed.ok ? { ok: true } : { ok: false, error: parsed.error };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { parseBlueprintData, validateBlueprintData };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 构造一份可交给 `initialWorkspace` 的空工作区。
|
|
65
|
+
*/
|
|
66
|
+
export function createEmptyWorkspace(
|
|
67
|
+
init?: Partial<WorkspaceProjectRecord>
|
|
68
|
+
): WorkspaceProjectRecord {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
return {
|
|
71
|
+
id: init?.id || createWorkspaceProjectId(),
|
|
72
|
+
name: init?.name ?? "",
|
|
73
|
+
createdAt: init?.createdAt ?? now,
|
|
74
|
+
updatedAt: init?.updatedAt ?? now,
|
|
75
|
+
panelState: init?.panelState ?? createEmptyPanelState(),
|
|
76
|
+
blueprintDocument: init?.blueprintDocument ?? BlueprintGraph.empty().document,
|
|
77
|
+
blueprintMeta: init?.blueprintMeta ?? { name: "", remark: "" },
|
|
78
|
+
productName: init?.productName ?? "",
|
|
79
|
+
titleIconDataUrl: init?.titleIconDataUrl,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 校验完整工作区数据:视图 `panelState` 与蓝图 `blueprintDocument` 都要能解析。
|
|
85
|
+
*/
|
|
86
|
+
export function validateWorkspaceData(data: unknown): WorkspaceParseCheckResult {
|
|
87
|
+
if (data == null) {
|
|
88
|
+
const empty = { ok: false, error: "empty" } satisfies ParseCheckResult;
|
|
89
|
+
return { ok: false, error: "empty", view: empty, blueprint: empty };
|
|
90
|
+
}
|
|
91
|
+
if (!isRecord(data)) {
|
|
92
|
+
const invalid = { ok: false, error: "invalid-workspace-data" } satisfies ParseCheckResult;
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: "invalid-workspace-data",
|
|
96
|
+
view: invalid,
|
|
97
|
+
blueprint: invalid,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const view = validateViewData(data);
|
|
101
|
+
const blueprint = validateBlueprintData(data);
|
|
102
|
+
const ok = view.ok && blueprint.ok;
|
|
103
|
+
return {
|
|
104
|
+
ok,
|
|
105
|
+
error: ok ? undefined : "invalid-workspace-data",
|
|
106
|
+
view,
|
|
107
|
+
blueprint,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 解析并规范化工作区。成功时 `value` 可直接作为 `initialWorkspace`。
|
|
113
|
+
*/
|
|
114
|
+
export function parseWorkspaceData(
|
|
115
|
+
data: unknown
|
|
116
|
+
): WorkspaceParseCheckResult & { value?: WorkspaceProjectRecord } {
|
|
117
|
+
const check = validateWorkspaceData(data);
|
|
118
|
+
if (!check.ok || !isRecord(data)) {
|
|
119
|
+
return check;
|
|
120
|
+
}
|
|
121
|
+
const view = parseViewData(data);
|
|
122
|
+
const blueprint = parseBlueprintData(data);
|
|
123
|
+
if (!view.value || !blueprint.value) {
|
|
124
|
+
return check;
|
|
125
|
+
}
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
return {
|
|
128
|
+
...check,
|
|
129
|
+
value: {
|
|
130
|
+
id: typeof data.id === "string" && data.id ? data.id : createWorkspaceProjectId(),
|
|
131
|
+
name: typeof data.name === "string" ? data.name : "",
|
|
132
|
+
createdAt: typeof data.createdAt === "number" ? data.createdAt : now,
|
|
133
|
+
updatedAt: typeof data.updatedAt === "number" ? data.updatedAt : now,
|
|
134
|
+
panelState: view.value,
|
|
135
|
+
blueprintDocument: blueprint.value as BlueprintDocument,
|
|
136
|
+
blueprintMeta: readBlueprintMeta(data.blueprintMeta),
|
|
137
|
+
productName: typeof data.productName === "string" ? data.productName : "",
|
|
138
|
+
titleIconDataUrl:
|
|
139
|
+
typeof data.titleIconDataUrl === "string" ? data.titleIconDataUrl : undefined,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
import { appStorageKey } from "@arronqzy/blueprint-dsl";
|
|
1
2
|
import type { WorkspaceProjectRecord } from "./workspace-project-db";
|
|
2
3
|
|
|
3
4
|
const CACHE_PREFIX = "arronqzy-workspace-preview:";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
function previewCacheKey(projectId: string, nameSpace?: string | null): string {
|
|
7
|
+
return appStorageKey(CACHE_PREFIX + projectId, nameSpace);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function writeWorkspacePreviewCache(
|
|
11
|
+
record: WorkspaceProjectRecord,
|
|
12
|
+
nameSpace?: string | null
|
|
13
|
+
): void {
|
|
6
14
|
try {
|
|
7
15
|
// 只写轻量标记,避免把含 data URL 的整包再塞进 localStorage(配额小、内存翻倍)
|
|
8
16
|
localStorage.setItem(
|
|
9
|
-
|
|
17
|
+
previewCacheKey(record.id, nameSpace),
|
|
10
18
|
JSON.stringify({ id: record.id, updatedAt: record.updatedAt })
|
|
11
19
|
);
|
|
12
20
|
} catch {
|
|
@@ -15,10 +23,11 @@ export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void
|
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
export function readWorkspacePreviewCache(
|
|
18
|
-
projectId: string
|
|
26
|
+
projectId: string,
|
|
27
|
+
nameSpace?: string | null
|
|
19
28
|
): WorkspaceProjectRecord | null {
|
|
20
29
|
try {
|
|
21
|
-
const raw = localStorage.getItem(
|
|
30
|
+
const raw = localStorage.getItem(previewCacheKey(projectId, nameSpace));
|
|
22
31
|
if (!raw) return null;
|
|
23
32
|
const parsed = JSON.parse(raw) as Partial<WorkspaceProjectRecord> | null;
|
|
24
33
|
if (!parsed || typeof parsed !== "object" || !parsed.panelState) return null;
|