@arronqzy/react-view 1.0.13 → 1.0.15
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 +11 -10
- package/src/panel/ReactViewOnlinePreview.tsx +31 -2
- package/src/panel/ReactViewPanel.tsx +186 -124
- 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 +140 -91
- package/src/panel/components/SelectLayer.tsx +5 -0
- package/src/panel/components/WorkspaceConfigSidebar.tsx +1 -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/gridPlacement.ts +29 -0
- package/src/panel/utils/panelElementDefaults.ts +37 -1
- package/src/panel/utils/scope-autocomplete.ts +0 -114
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
workspaceSnapshotsEqual,
|
|
20
20
|
type WorkspaceSnapshot,
|
|
21
21
|
} from "../library/workspace-snapshot";
|
|
22
|
+
import { runBusyTask } from "../utils/async-work";
|
|
22
23
|
|
|
23
24
|
export type UseWorkspaceProjectsOptions = {
|
|
24
25
|
exportPanelData: () => State;
|
|
@@ -69,6 +70,29 @@ export function useWorkspaceProjects({
|
|
|
69
70
|
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
|
70
71
|
const [activeProjectName, setActiveProjectName] = useState<string | null>(null);
|
|
71
72
|
const syncedSnapshotRef = useRef<WorkspaceSnapshot | null>(null);
|
|
73
|
+
const syncedPanelRevisionRef = useRef<unknown>(null);
|
|
74
|
+
const pendingRevisionSyncRef = useRef(false);
|
|
75
|
+
const previewWindowsRef = useRef(new Map<string, Window>());
|
|
76
|
+
const [previewingProjectIds, setPreviewingProjectIds] = useState<string[]>([]);
|
|
77
|
+
|
|
78
|
+
const refreshPreviewing = useCallback(() => {
|
|
79
|
+
const ids: string[] = [];
|
|
80
|
+
for (const [id, win] of previewWindowsRef.current) {
|
|
81
|
+
if (!win.closed) ids.push(id);
|
|
82
|
+
else previewWindowsRef.current.delete(id);
|
|
83
|
+
}
|
|
84
|
+
setPreviewingProjectIds((prev) => {
|
|
85
|
+
if (prev.length === ids.length && prev.every((value, index) => value === ids[index])) {
|
|
86
|
+
return prev;
|
|
87
|
+
}
|
|
88
|
+
return ids;
|
|
89
|
+
});
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const timer = window.setInterval(refreshPreviewing, 2000);
|
|
94
|
+
return () => window.clearInterval(timer);
|
|
95
|
+
}, [refreshPreviewing]);
|
|
72
96
|
|
|
73
97
|
const resolveProjectName = useCallback(() => {
|
|
74
98
|
return productName.trim() || t("panel.defaults.unnamedProduct");
|
|
@@ -76,19 +100,12 @@ export function useWorkspaceProjects({
|
|
|
76
100
|
|
|
77
101
|
const buildCurrentSnapshot = useCallback((): WorkspaceSnapshot => {
|
|
78
102
|
return {
|
|
79
|
-
panelState: exportPanelData(),
|
|
80
103
|
blueprintDocument,
|
|
81
104
|
blueprintMeta: { ...blueprintMeta },
|
|
82
105
|
productName,
|
|
83
106
|
titleIconDataUrl,
|
|
84
107
|
};
|
|
85
|
-
}, [
|
|
86
|
-
blueprintDocument,
|
|
87
|
-
blueprintMeta,
|
|
88
|
-
exportPanelData,
|
|
89
|
-
productName,
|
|
90
|
-
titleIconDataUrl,
|
|
91
|
-
]);
|
|
108
|
+
}, [blueprintDocument, blueprintMeta, productName, titleIconDataUrl]);
|
|
92
109
|
|
|
93
110
|
const refreshProjects = useCallback(async () => {
|
|
94
111
|
const items = await listWorkspaceProjects();
|
|
@@ -99,8 +116,16 @@ export function useWorkspaceProjects({
|
|
|
99
116
|
void refreshProjects();
|
|
100
117
|
}, [refreshProjects]);
|
|
101
118
|
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!pendingRevisionSyncRef.current) return;
|
|
121
|
+
syncedPanelRevisionRef.current = panelRevision;
|
|
122
|
+
pendingRevisionSyncRef.current = false;
|
|
123
|
+
}, [panelRevision]);
|
|
124
|
+
|
|
102
125
|
const dirty = useMemo(() => {
|
|
103
126
|
if (!activeProjectId || !syncedSnapshotRef.current) return false;
|
|
127
|
+
if (pendingRevisionSyncRef.current) return false;
|
|
128
|
+
if (panelRevision !== syncedPanelRevisionRef.current) return true;
|
|
104
129
|
return !workspaceSnapshotsEqual(buildCurrentSnapshot(), syncedSnapshotRef.current);
|
|
105
130
|
}, [
|
|
106
131
|
activeProjectId,
|
|
@@ -129,12 +154,13 @@ export function useWorkspaceProjects({
|
|
|
129
154
|
setActiveProjectId(record.id);
|
|
130
155
|
setActiveProjectName(record.name);
|
|
131
156
|
syncedSnapshotRef.current = cloneWorkspaceSnapshot({
|
|
132
|
-
panelState: record.panelState,
|
|
133
157
|
blueprintDocument,
|
|
134
158
|
blueprintMeta,
|
|
135
159
|
productName: record.productName,
|
|
136
160
|
titleIconDataUrl: record.titleIconDataUrl ?? "",
|
|
137
161
|
});
|
|
162
|
+
syncedPanelRevisionRef.current = panelRevision;
|
|
163
|
+
pendingRevisionSyncRef.current = true;
|
|
138
164
|
onProjectApplied?.({
|
|
139
165
|
...record,
|
|
140
166
|
blueprintDocument,
|
|
@@ -148,13 +174,14 @@ export function useWorkspaceProjects({
|
|
|
148
174
|
setBlueprintMeta,
|
|
149
175
|
setProductName,
|
|
150
176
|
setTitleIconDataUrl,
|
|
177
|
+
panelRevision,
|
|
151
178
|
t,
|
|
152
179
|
]
|
|
153
180
|
);
|
|
154
181
|
|
|
155
182
|
const persistProject = useCallback(
|
|
156
183
|
async (options: { id?: string; name: string; createdAt?: number }) => {
|
|
157
|
-
const snapshot = buildCurrentSnapshot();
|
|
184
|
+
const snapshot = cloneWorkspaceSnapshot(buildCurrentSnapshot());
|
|
158
185
|
const now = Date.now();
|
|
159
186
|
const id = options.id ?? createWorkspaceProjectId();
|
|
160
187
|
const createdAt = options.createdAt ?? now;
|
|
@@ -165,7 +192,7 @@ export function useWorkspaceProjects({
|
|
|
165
192
|
name,
|
|
166
193
|
createdAt,
|
|
167
194
|
updatedAt: now,
|
|
168
|
-
panelState:
|
|
195
|
+
panelState: exportPanelData(),
|
|
169
196
|
blueprintDocument: snapshot.blueprintDocument,
|
|
170
197
|
blueprintMeta: snapshot.blueprintMeta,
|
|
171
198
|
productName: snapshot.productName,
|
|
@@ -177,92 +204,131 @@ export function useWorkspaceProjects({
|
|
|
177
204
|
broadcastWorkspaceProjectUpdate(id, now);
|
|
178
205
|
setActiveProjectId(id);
|
|
179
206
|
setActiveProjectName(name);
|
|
180
|
-
syncedSnapshotRef.current =
|
|
207
|
+
syncedSnapshotRef.current = snapshot;
|
|
208
|
+
syncedPanelRevisionRef.current = panelRevision;
|
|
209
|
+
pendingRevisionSyncRef.current = false;
|
|
181
210
|
await refreshProjects();
|
|
182
211
|
return record;
|
|
183
212
|
},
|
|
184
|
-
[buildCurrentSnapshot, resolveProjectName, refreshProjects]
|
|
213
|
+
[buildCurrentSnapshot, exportPanelData, panelRevision, resolveProjectName, refreshProjects]
|
|
185
214
|
);
|
|
186
215
|
|
|
187
216
|
const handleCreateProject = useCallback(async () => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
217
|
+
return runBusyTask(t("common.savingWorkspace"), async () => {
|
|
218
|
+
const name = resolveProjectName();
|
|
219
|
+
const record = await persistProject({ name });
|
|
220
|
+
return { name, id: record.id };
|
|
221
|
+
});
|
|
222
|
+
}, [persistProject, resolveProjectName, t]);
|
|
192
223
|
|
|
193
224
|
const handleSaveProject = useCallback(async () => {
|
|
194
225
|
if (!activeProjectId) {
|
|
195
226
|
return handleCreateProject();
|
|
196
227
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
228
|
+
return runBusyTask(t("common.savingWorkspace"), async () => {
|
|
229
|
+
const existing = await getWorkspaceProject(activeProjectId);
|
|
230
|
+
const name = resolveProjectName();
|
|
231
|
+
const record = await persistProject({
|
|
232
|
+
id: activeProjectId,
|
|
233
|
+
name,
|
|
234
|
+
createdAt: existing?.createdAt,
|
|
235
|
+
});
|
|
236
|
+
return { name, id: record.id };
|
|
203
237
|
});
|
|
204
|
-
|
|
205
|
-
}, [activeProjectId, handleCreateProject, persistProject, resolveProjectName]);
|
|
238
|
+
}, [activeProjectId, handleCreateProject, persistProject, resolveProjectName, t]);
|
|
206
239
|
|
|
207
240
|
const handleOpenProject = useCallback(
|
|
208
241
|
async (id: string) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
242
|
+
await runBusyTask(t("common.loadingWorkspace"), async () => {
|
|
243
|
+
const record = await getWorkspaceProject(id);
|
|
244
|
+
if (!record) {
|
|
245
|
+
await refreshProjects();
|
|
246
|
+
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
247
|
+
}
|
|
248
|
+
applyProjectRecord(record);
|
|
249
|
+
});
|
|
215
250
|
},
|
|
216
251
|
[applyProjectRecord, refreshProjects, t]
|
|
217
252
|
);
|
|
218
253
|
|
|
219
254
|
const handleSyncProject = useCallback(async () => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
255
|
+
return runBusyTask(t("common.syncingWorkspace"), async () => {
|
|
256
|
+
if (!activeProjectId) {
|
|
257
|
+
throw new Error(t("panel.messages.saveWorkspaceFirst"));
|
|
258
|
+
}
|
|
259
|
+
const existing = await getWorkspaceProject(activeProjectId);
|
|
260
|
+
if (!existing) {
|
|
261
|
+
setActiveProjectId(null);
|
|
262
|
+
setActiveProjectName(null);
|
|
263
|
+
syncedSnapshotRef.current = null;
|
|
264
|
+
await refreshProjects();
|
|
265
|
+
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
266
|
+
}
|
|
267
|
+
const name = resolveProjectName() || existing.name;
|
|
268
|
+
await persistProject({
|
|
269
|
+
id: activeProjectId,
|
|
270
|
+
name,
|
|
271
|
+
createdAt: existing.createdAt,
|
|
272
|
+
});
|
|
273
|
+
return name;
|
|
236
274
|
});
|
|
237
|
-
return name;
|
|
238
275
|
}, [activeProjectId, persistProject, refreshProjects, resolveProjectName, t]);
|
|
239
276
|
|
|
240
277
|
const handleDeleteProject = useCallback(
|
|
241
278
|
async (id: string) => {
|
|
242
|
-
await
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
279
|
+
await runBusyTask(t("common.deletingWorkspace"), async () => {
|
|
280
|
+
await deleteWorkspaceProject(id);
|
|
281
|
+
if (activeProjectId === id) {
|
|
282
|
+
setActiveProjectId(null);
|
|
283
|
+
setActiveProjectName(null);
|
|
284
|
+
syncedSnapshotRef.current = null;
|
|
285
|
+
}
|
|
286
|
+
previewWindowsRef.current.delete(id);
|
|
287
|
+
refreshPreviewing();
|
|
288
|
+
await refreshProjects();
|
|
289
|
+
});
|
|
249
290
|
},
|
|
250
|
-
[activeProjectId, refreshProjects]
|
|
291
|
+
[activeProjectId, refreshPreviewing, refreshProjects, t]
|
|
251
292
|
);
|
|
252
293
|
|
|
253
294
|
const handlePreviewProject = useCallback(
|
|
254
295
|
async (id: string, options?: { syncFirst?: boolean }) => {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
296
|
+
await runBusyTask(t("common.openingPreview"), async () => {
|
|
297
|
+
if (options?.syncFirst && activeProjectId === id && dirty) {
|
|
298
|
+
await handleSyncProject();
|
|
299
|
+
}
|
|
300
|
+
const record =
|
|
301
|
+
(await getWorkspaceProject(id)) ?? readWorkspacePreviewCache(id);
|
|
302
|
+
if (record) {
|
|
303
|
+
writeWorkspacePreviewCache(record);
|
|
304
|
+
}
|
|
305
|
+
const existing = previewWindowsRef.current.get(id);
|
|
306
|
+
if (existing && !existing.closed) {
|
|
307
|
+
try {
|
|
308
|
+
existing.focus();
|
|
309
|
+
} catch {
|
|
310
|
+
/* ignore */
|
|
311
|
+
}
|
|
312
|
+
refreshPreviewing();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const win = window.open(
|
|
316
|
+
buildOnlinePreviewUrl(id),
|
|
317
|
+
`abuilder-preview-${id}`
|
|
318
|
+
);
|
|
319
|
+
if (win) {
|
|
320
|
+
previewWindowsRef.current.set(id, win);
|
|
321
|
+
try {
|
|
322
|
+
win.opener = null;
|
|
323
|
+
win.focus();
|
|
324
|
+
} catch {
|
|
325
|
+
/* ignore */
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
refreshPreviewing();
|
|
329
|
+
});
|
|
264
330
|
},
|
|
265
|
-
[activeProjectId, dirty, handleSyncProject]
|
|
331
|
+
[activeProjectId, dirty, handleSyncProject, refreshPreviewing, t]
|
|
266
332
|
);
|
|
267
333
|
|
|
268
334
|
return {
|
|
@@ -270,6 +336,7 @@ export function useWorkspaceProjects({
|
|
|
270
336
|
activeProjectId,
|
|
271
337
|
activeProjectName,
|
|
272
338
|
dirty,
|
|
339
|
+
previewingProjectIds,
|
|
273
340
|
handleCreateProject,
|
|
274
341
|
handleSaveProject,
|
|
275
342
|
handleOpenProject,
|
|
@@ -4,7 +4,11 @@ const CACHE_PREFIX = "arronqzy-workspace-preview:";
|
|
|
4
4
|
|
|
5
5
|
export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void {
|
|
6
6
|
try {
|
|
7
|
-
|
|
7
|
+
// 只写轻量标记,避免把含 data URL 的整包再塞进 localStorage(配额小、内存翻倍)
|
|
8
|
+
localStorage.setItem(
|
|
9
|
+
CACHE_PREFIX + record.id,
|
|
10
|
+
JSON.stringify({ id: record.id, updatedAt: record.updatedAt })
|
|
11
|
+
);
|
|
8
12
|
} catch {
|
|
9
13
|
// ignore quota / private mode
|
|
10
14
|
}
|
|
@@ -16,7 +20,9 @@ export function readWorkspacePreviewCache(
|
|
|
16
20
|
try {
|
|
17
21
|
const raw = localStorage.getItem(CACHE_PREFIX + projectId);
|
|
18
22
|
if (!raw) return null;
|
|
19
|
-
|
|
23
|
+
const parsed = JSON.parse(raw) as Partial<WorkspaceProjectRecord> | null;
|
|
24
|
+
if (!parsed || typeof parsed !== "object" || !parsed.panelState) return null;
|
|
25
|
+
return parsed as WorkspaceProjectRecord;
|
|
20
26
|
} catch {
|
|
21
27
|
return null;
|
|
22
28
|
}
|
|
@@ -78,17 +78,35 @@ export function createWorkspaceProjectId(): string {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[]> {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
81
|
+
const db = await openDb();
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
84
|
+
const store = tx.objectStore(STORE_NAME);
|
|
85
|
+
const request = store.openCursor();
|
|
86
|
+
const items: WorkspaceProjectListItem[] = [];
|
|
87
|
+
request.onerror = () =>
|
|
88
|
+
reject(request.error ?? new Error("indexeddb-request-failed"));
|
|
89
|
+
request.onsuccess = () => {
|
|
90
|
+
const cursor = request.result;
|
|
91
|
+
if (!cursor) return;
|
|
92
|
+
const record = cursor.value as WorkspaceProjectRecord;
|
|
93
|
+
items.push({
|
|
94
|
+
id: record.id,
|
|
95
|
+
name: record.name,
|
|
96
|
+
createdAt: record.createdAt,
|
|
97
|
+
updatedAt: record.updatedAt,
|
|
98
|
+
});
|
|
99
|
+
cursor.continue();
|
|
100
|
+
};
|
|
101
|
+
tx.oncomplete = () => {
|
|
102
|
+
db.close();
|
|
103
|
+
resolve(items.sort((a, b) => b.updatedAt - a.updatedAt));
|
|
104
|
+
};
|
|
105
|
+
tx.onerror = () => {
|
|
106
|
+
db.close();
|
|
107
|
+
reject(tx.error ?? new Error("indexeddb-transaction-failed"));
|
|
108
|
+
};
|
|
109
|
+
});
|
|
92
110
|
}
|
|
93
111
|
|
|
94
112
|
export async function getWorkspaceProject(
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { blueprintDocumentsEqual } from "@arronqzy/react-blueprint";
|
|
2
2
|
import type { BlueprintDocument, BlueprintMetaDraft } from "@arronqzy/react-blueprint";
|
|
3
|
-
import type { State } from "@arronqzy/rx-store";
|
|
4
3
|
|
|
5
4
|
export type WorkspaceSnapshot = {
|
|
6
|
-
panelState: State;
|
|
7
5
|
blueprintDocument: BlueprintDocument;
|
|
8
6
|
blueprintMeta: BlueprintMetaDraft;
|
|
9
7
|
productName: string;
|
|
10
8
|
titleIconDataUrl: string;
|
|
11
9
|
};
|
|
12
10
|
|
|
11
|
+
function cloneBlueprintDocument(document: BlueprintDocument): BlueprintDocument {
|
|
12
|
+
if (typeof structuredClone === "function") {
|
|
13
|
+
try {
|
|
14
|
+
return structuredClone(document);
|
|
15
|
+
} catch {
|
|
16
|
+
/* fall through */
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(JSON.stringify(document)) as BlueprintDocument;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
export function workspaceSnapshotsEqual(
|
|
14
23
|
a: WorkspaceSnapshot,
|
|
15
24
|
b: WorkspaceSnapshot
|
|
@@ -19,9 +28,17 @@ export function workspaceSnapshotsEqual(
|
|
|
19
28
|
if (a.blueprintMeta.name !== b.blueprintMeta.name) return false;
|
|
20
29
|
if ((a.blueprintMeta.remark ?? "") !== (b.blueprintMeta.remark ?? "")) return false;
|
|
21
30
|
if (!blueprintDocumentsEqual(a.blueprintDocument, b.blueprintDocument)) return false;
|
|
22
|
-
return
|
|
31
|
+
return true;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
export function cloneWorkspaceSnapshot(snapshot: WorkspaceSnapshot): WorkspaceSnapshot {
|
|
26
|
-
return
|
|
35
|
+
return {
|
|
36
|
+
blueprintDocument: cloneBlueprintDocument(snapshot.blueprintDocument),
|
|
37
|
+
blueprintMeta: {
|
|
38
|
+
name: snapshot.blueprintMeta.name,
|
|
39
|
+
remark: snapshot.blueprintMeta.remark ?? "",
|
|
40
|
+
},
|
|
41
|
+
productName: snapshot.productName,
|
|
42
|
+
titleIconDataUrl: snapshot.titleIconDataUrl,
|
|
43
|
+
};
|
|
27
44
|
}
|
|
@@ -20,6 +20,14 @@ export function clearViewElementScopes() {
|
|
|
20
20
|
emit();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export function removeViewElementScopes(elementIds: readonly string[]) {
|
|
24
|
+
let changed = false;
|
|
25
|
+
for (const id of elementIds) {
|
|
26
|
+
if (scopes.delete(id)) changed = true;
|
|
27
|
+
}
|
|
28
|
+
if (changed) emit();
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export function setViewElementScope(elementId: string, scope: unknown) {
|
|
24
32
|
scopes.set(elementId, scope);
|
|
25
33
|
emit();
|
package/src/panel/types.ts
CHANGED
|
@@ -54,8 +54,8 @@ export type PanelElementStyle = {
|
|
|
54
54
|
|
|
55
55
|
export type ReferenceCopyMode = "shallow" | "deep";
|
|
56
56
|
|
|
57
|
-
export type { PanelTableConfig } from "@arronqzy/view-table";
|
|
58
57
|
import type { PanelTableConfig } from "@arronqzy/view-table";
|
|
58
|
+
import type { Scene3dConfig } from "@arronqzy/view-scene3d";
|
|
59
59
|
|
|
60
60
|
export type PanelElement = {
|
|
61
61
|
id: string;
|
|
@@ -75,8 +75,8 @@ export type PanelElement = {
|
|
|
75
75
|
textFontSize?: number;
|
|
76
76
|
textFontWeight?: string;
|
|
77
77
|
textColor?: string;
|
|
78
|
-
textLineHeight?: number;
|
|
79
|
-
textAlign?: "left" | "center" | "right" | "justify";
|
|
78
|
+
textLineHeight?: number | "auto";
|
|
79
|
+
textAlign?: "left" | "center" | "right" | "justify" | "middle";
|
|
80
80
|
gridRows?: number;
|
|
81
81
|
gridCols?: number;
|
|
82
82
|
gridGap?: number;
|
|
@@ -101,6 +101,7 @@ export type PanelElement = {
|
|
|
101
101
|
geometryColor?: string;
|
|
102
102
|
geometryScript?: string;
|
|
103
103
|
geometrySketchDataUrl?: string;
|
|
104
|
+
scene3d?: Scene3dConfig;
|
|
104
105
|
style?: PanelElementStyle;
|
|
105
106
|
x: number;
|
|
106
107
|
y: number;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { resolveLocale, tForLocale } from "@arronqzy/i18n";
|
|
3
|
+
|
|
4
|
+
export type MediaFileKind = "image" | "audio" | "video" | "json" | "model3d";
|
|
5
|
+
|
|
6
|
+
export const FILE_SIZE_LIMITS: Record<MediaFileKind, number> = {
|
|
7
|
+
image: 8 * 1024 * 1024,
|
|
8
|
+
audio: 20 * 1024 * 1024,
|
|
9
|
+
video: 40 * 1024 * 1024,
|
|
10
|
+
json: 24 * 1024 * 1024,
|
|
11
|
+
model3d: 48 * 1024 * 1024,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const JSON_WORKER_THRESHOLD = 128 * 1024;
|
|
15
|
+
|
|
16
|
+
let busyMessage: string | null = null;
|
|
17
|
+
let busyDepth = 0;
|
|
18
|
+
const busyListeners = new Set<() => void>();
|
|
19
|
+
|
|
20
|
+
function emitBusy() {
|
|
21
|
+
busyListeners.forEach((listener) => listener());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function setBusyMessage(message: string | null) {
|
|
25
|
+
busyMessage = message;
|
|
26
|
+
emitBusy();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getBusyOverlayMessage() {
|
|
30
|
+
return busyMessage;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function subscribeBusyOverlay(listener: () => void) {
|
|
34
|
+
busyListeners.add(listener);
|
|
35
|
+
return () => busyListeners.delete(listener);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useBusyOverlayMessage() {
|
|
39
|
+
return useSyncExternalStore(
|
|
40
|
+
subscribeBusyOverlay,
|
|
41
|
+
getBusyOverlayMessage,
|
|
42
|
+
() => null
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function yieldToPaint(): Promise<void> {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
49
|
+
setTimeout(resolve, 0);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
requestAnimationFrame(() => {
|
|
53
|
+
requestAnimationFrame(() => resolve());
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function runBusyTask<T>(
|
|
59
|
+
message: string,
|
|
60
|
+
task: () => Promise<T> | T
|
|
61
|
+
): Promise<T> {
|
|
62
|
+
const previous = busyMessage;
|
|
63
|
+
busyDepth += 1;
|
|
64
|
+
setBusyMessage(message);
|
|
65
|
+
await yieldToPaint();
|
|
66
|
+
try {
|
|
67
|
+
return await task();
|
|
68
|
+
} finally {
|
|
69
|
+
busyDepth = Math.max(0, busyDepth - 1);
|
|
70
|
+
setBusyMessage(busyDepth === 0 ? null : previous);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatFileSize(bytes: number) {
|
|
75
|
+
if (bytes >= 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
76
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function assertFileSize(file: Blob, kind: MediaFileKind) {
|
|
80
|
+
const limit = FILE_SIZE_LIMITS[kind];
|
|
81
|
+
if (file.size <= limit) return;
|
|
82
|
+
const t = tForLocale(resolveLocale());
|
|
83
|
+
throw new Error(
|
|
84
|
+
t("common.fileTooLarge", { max: formatFileSize(limit) })
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function readFileAsDataUrl(
|
|
89
|
+
file: Blob,
|
|
90
|
+
errorMessage: string,
|
|
91
|
+
kind: MediaFileKind = "image"
|
|
92
|
+
): Promise<string> {
|
|
93
|
+
assertFileSize(file, kind);
|
|
94
|
+
const t = tForLocale(resolveLocale());
|
|
95
|
+
return runBusyTask(t("common.uploadingFile"), () => {
|
|
96
|
+
return new Promise<string>((resolve, reject) => {
|
|
97
|
+
const reader = new FileReader();
|
|
98
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
99
|
+
reader.onerror = () => reject(new Error(errorMessage));
|
|
100
|
+
reader.readAsDataURL(file);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let jsonWorker: Worker | null = null;
|
|
106
|
+
let jsonWorkerObjectUrl: string | null = null;
|
|
107
|
+
let jsonRequestId = 0;
|
|
108
|
+
const jsonPending = new Map<
|
|
109
|
+
number,
|
|
110
|
+
{ resolve: (value: unknown) => void; reject: (error: Error) => void }
|
|
111
|
+
>();
|
|
112
|
+
|
|
113
|
+
function ensureJsonWorker(): Worker | null {
|
|
114
|
+
if (typeof Worker === "undefined") return null;
|
|
115
|
+
if (jsonWorker) return jsonWorker;
|
|
116
|
+
const source = `
|
|
117
|
+
self.onmessage = (event) => {
|
|
118
|
+
const { id, text } = event.data || {};
|
|
119
|
+
try {
|
|
120
|
+
self.postMessage({ id, ok: true, value: JSON.parse(text) });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
self.postMessage({
|
|
123
|
+
id,
|
|
124
|
+
ok: false,
|
|
125
|
+
error: error instanceof Error ? error.message : String(error),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
`;
|
|
130
|
+
jsonWorkerObjectUrl = URL.createObjectURL(
|
|
131
|
+
new Blob([source], { type: "text/javascript" })
|
|
132
|
+
);
|
|
133
|
+
jsonWorker = new Worker(jsonWorkerObjectUrl);
|
|
134
|
+
jsonWorker.onmessage = (event: MessageEvent) => {
|
|
135
|
+
const { id, ok, value, error } = event.data ?? {};
|
|
136
|
+
const pending = jsonPending.get(id);
|
|
137
|
+
if (!pending) return;
|
|
138
|
+
jsonPending.delete(id);
|
|
139
|
+
if (ok) pending.resolve(value);
|
|
140
|
+
else pending.reject(new Error(String(error ?? "json-parse-failed")));
|
|
141
|
+
};
|
|
142
|
+
jsonWorker.onerror = () => {
|
|
143
|
+
jsonPending.forEach(({ reject }) => reject(new Error("json-worker-failed")));
|
|
144
|
+
jsonPending.clear();
|
|
145
|
+
disposeJsonWorker();
|
|
146
|
+
};
|
|
147
|
+
if (typeof window !== "undefined") {
|
|
148
|
+
window.addEventListener("pagehide", disposeJsonWorker, { once: true });
|
|
149
|
+
}
|
|
150
|
+
return jsonWorker;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function disposeJsonWorker() {
|
|
154
|
+
jsonPending.forEach(({ reject }) => reject(new Error("json-worker-disposed")));
|
|
155
|
+
jsonPending.clear();
|
|
156
|
+
jsonWorker?.terminate();
|
|
157
|
+
jsonWorker = null;
|
|
158
|
+
if (jsonWorkerObjectUrl) {
|
|
159
|
+
URL.revokeObjectURL(jsonWorkerObjectUrl);
|
|
160
|
+
jsonWorkerObjectUrl = null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function parseJsonText<T>(text: string): Promise<T> {
|
|
165
|
+
if (text.length < JSON_WORKER_THRESHOLD) {
|
|
166
|
+
return JSON.parse(text) as T;
|
|
167
|
+
}
|
|
168
|
+
const worker = ensureJsonWorker();
|
|
169
|
+
if (!worker) return JSON.parse(text) as T;
|
|
170
|
+
const id = ++jsonRequestId;
|
|
171
|
+
return new Promise<T>((resolve, reject) => {
|
|
172
|
+
jsonPending.set(id, {
|
|
173
|
+
resolve: (value) => resolve(value as T),
|
|
174
|
+
reject,
|
|
175
|
+
});
|
|
176
|
+
worker.postMessage({ id, text });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -187,3 +187,32 @@ export function computeSnapPatchForNewElementOnLayer(
|
|
|
187
187
|
height: nearest.height,
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
|
+
|
|
191
|
+
function isGridAncestorOf(
|
|
192
|
+
ancestorId: string,
|
|
193
|
+
el: PanelElement,
|
|
194
|
+
byId: Map<string, PanelElement>
|
|
195
|
+
): boolean {
|
|
196
|
+
let current: PanelElement | undefined = el;
|
|
197
|
+
const seen = new Set<string>();
|
|
198
|
+
while (current?.parentGridId && !seen.has(current.id)) {
|
|
199
|
+
if (current.parentGridId === ancestorId) return true;
|
|
200
|
+
seen.add(current.id);
|
|
201
|
+
current = byId.get(current.parentGridId);
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 父网格先于格子内子节点绘制,避免网格占位层盖住内容 */
|
|
207
|
+
export function comparePanelElementsPaintOrder(
|
|
208
|
+
a: PanelElement,
|
|
209
|
+
b: PanelElement,
|
|
210
|
+
byId: Map<string, PanelElement>
|
|
211
|
+
): number {
|
|
212
|
+
if (isGridAncestorOf(a.id, b, byId)) return -1;
|
|
213
|
+
if (isGridAncestorOf(b.id, a, byId)) return 1;
|
|
214
|
+
const za = a.zIndex ?? 1;
|
|
215
|
+
const zb = b.zIndex ?? 1;
|
|
216
|
+
if (za !== zb) return za - zb;
|
|
217
|
+
return a.id.localeCompare(b.id);
|
|
218
|
+
}
|