@arronqzy/vue-view 0.1.6 → 0.1.7
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 +7 -6
- package/src/panel/VueViewOnlinePreview.vue +28 -2
- package/src/panel/VueViewPanel.vue +120 -66
- package/src/panel/components/BusyOverlay.vue +24 -0
- package/src/panel/components/ConfigHintIcon.vue +10 -10
- package/src/panel/components/ElementsLayer.vue +51 -8
- package/src/panel/components/MaterialPreview.vue +8 -0
- package/src/panel/components/PanelConfigSidebar.vue +11 -0
- package/src/panel/components/WorkspaceConfigSidebar.vue +1 -0
- package/src/panel/components/WorkspaceProjectNav.vue +36 -8
- package/src/panel/components/config/ConfigColorField.vue +33 -9
- package/src/panel/components/config/PanelConfigAudioSection.vue +32 -18
- package/src/panel/components/config/PanelConfigImageSection.vue +13 -9
- package/src/panel/components/config/PanelConfigScene3dSection.vue +505 -0
- package/src/panel/components/config/PanelConfigStyleSections.vue +13 -9
- package/src/panel/components/config/PanelConfigTextSection.vue +20 -6
- package/src/panel/components/config/PanelConfigVideoSection.vue +13 -9
- package/src/panel/components/config/shared.ts +16 -21
- package/src/panel/components/elementsLayerNodes.ts +64 -59
- package/src/panel/components/materialSidebarData.ts +2 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +1 -1
- package/src/panel/components/table/PanelConfigTableSection.vue +39 -25
- package/src/panel/components/table/TableNodeContent.vue +36 -15
- package/src/panel/hooks/usePanelElements.ts +13 -1
- package/src/panel/hooks/useWorkspaceProjects.ts +128 -57
- package/src/panel/library/workspace-project-cache.ts +7 -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 -2
- package/src/panel/utils/async-work.ts +153 -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
|
@@ -10,12 +10,14 @@ import type {
|
|
|
10
10
|
ReferenceCopyMode,
|
|
11
11
|
} from "../types";
|
|
12
12
|
import { getPanelMessages } from "../constants/messages";
|
|
13
|
+
import { removeViewElementScopes } from "../scope/view-scope-store";
|
|
13
14
|
import {
|
|
14
15
|
DEFAULT_LAYER,
|
|
15
16
|
DEFAULT_LAYER_ID,
|
|
16
17
|
getDefaultChartConfig,
|
|
17
18
|
getDefaultGridConfig,
|
|
18
19
|
getDefaultNodeName,
|
|
20
|
+
getDefaultScene3dConfigForMaterial,
|
|
19
21
|
getDefaultSizeByMaterial,
|
|
20
22
|
getDefaultTableConfig,
|
|
21
23
|
getDefaultTextContent,
|
|
@@ -385,6 +387,7 @@ export function usePanelElements() {
|
|
|
385
387
|
});
|
|
386
388
|
const idSet = new Set(unlockedIds);
|
|
387
389
|
if (idSet.size === 0) return;
|
|
390
|
+
const familyIds = new Set<string>();
|
|
388
391
|
store.update(
|
|
389
392
|
(draft) => {
|
|
390
393
|
const deletedSourceIds = new Set<string>();
|
|
@@ -394,7 +397,7 @@ export function usePanelElements() {
|
|
|
394
397
|
const props = n.props as PanelElement;
|
|
395
398
|
deletedSourceIds.add(props.mappingSourceNodeId ?? props.id);
|
|
396
399
|
});
|
|
397
|
-
|
|
400
|
+
familyIds.clear();
|
|
398
401
|
(draft.root.children ?? []).forEach((n) => {
|
|
399
402
|
if (!isPanelElementNode(n) || !n.props) return;
|
|
400
403
|
const props = n.props as PanelElement;
|
|
@@ -407,6 +410,7 @@ export function usePanelElements() {
|
|
|
407
410
|
},
|
|
408
411
|
{ meta: { type: "node.batch-delete", ids: Array.from(idSet) } }
|
|
409
412
|
);
|
|
413
|
+
removeViewElementScopes([...familyIds]);
|
|
410
414
|
}
|
|
411
415
|
|
|
412
416
|
function bringElementsToFront(ids: string[]) {
|
|
@@ -613,6 +617,7 @@ export function usePanelElements() {
|
|
|
613
617
|
geometryShape: materialType === "geometry" ? "rect" : undefined,
|
|
614
618
|
geometryColor: materialType === "geometry" ? "#3b82f6" : undefined,
|
|
615
619
|
geometryScript: undefined,
|
|
620
|
+
...getDefaultScene3dConfigForMaterial(materialType),
|
|
616
621
|
x: Math.round(x - size.width / 2),
|
|
617
622
|
y: Math.round(y - size.height / 2),
|
|
618
623
|
width: size.width,
|
|
@@ -1029,6 +1034,13 @@ export function usePanelElements() {
|
|
|
1029
1034
|
|
|
1030
1035
|
function exportPanelData() {
|
|
1031
1036
|
const current = store.getState();
|
|
1037
|
+
if (typeof structuredClone === "function") {
|
|
1038
|
+
try {
|
|
1039
|
+
return structuredClone(current) as State;
|
|
1040
|
+
} catch {
|
|
1041
|
+
/* fall through */
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1032
1044
|
return JSON.parse(JSON.stringify(current)) as State;
|
|
1033
1045
|
}
|
|
1034
1046
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, onMounted, ref, shallowRef, toValue, type MaybeRefOrGetter } from "vue";
|
|
1
|
+
import { computed, onMounted, onUnmounted, ref, shallowRef, toValue, watch, type MaybeRefOrGetter } from "vue";
|
|
2
2
|
import {
|
|
3
3
|
BlueprintGraph,
|
|
4
4
|
type BlueprintDocument,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
workspaceSnapshotsEqual,
|
|
23
23
|
type WorkspaceSnapshot,
|
|
24
24
|
} from "../library/workspace-snapshot";
|
|
25
|
+
import { runBusyTask } from "../utils/async-work";
|
|
25
26
|
|
|
26
27
|
export type UseWorkspaceProjectsOptions = {
|
|
27
28
|
exportPanelData: () => State;
|
|
@@ -59,6 +60,20 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
59
60
|
const activeProjectId = ref<string | null>(null);
|
|
60
61
|
const activeProjectName = ref<string | null>(null);
|
|
61
62
|
const syncedSnapshotRef = shallowRef<WorkspaceSnapshot | null>(null);
|
|
63
|
+
let syncedPanelRevision: unknown = null;
|
|
64
|
+
let pendingRevisionSync = false;
|
|
65
|
+
const previewWindows = new Map<string, Window>();
|
|
66
|
+
const previewingProjectIds = ref<string[]>([]);
|
|
67
|
+
let previewPollTimer: number | undefined;
|
|
68
|
+
|
|
69
|
+
function refreshPreviewing() {
|
|
70
|
+
const ids: string[] = [];
|
|
71
|
+
for (const [id, win] of previewWindows) {
|
|
72
|
+
if (!win.closed) ids.push(id);
|
|
73
|
+
else previewWindows.delete(id);
|
|
74
|
+
}
|
|
75
|
+
previewingProjectIds.value = ids;
|
|
76
|
+
}
|
|
62
77
|
|
|
63
78
|
function resolveProjectName() {
|
|
64
79
|
void locale.value;
|
|
@@ -67,7 +82,6 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
67
82
|
|
|
68
83
|
function buildCurrentSnapshot(): WorkspaceSnapshot {
|
|
69
84
|
return {
|
|
70
|
-
panelState: options.exportPanelData(),
|
|
71
85
|
blueprintDocument: toValue(options.blueprintDocument),
|
|
72
86
|
blueprintMeta: { ...toValue(options.blueprintMeta) },
|
|
73
87
|
productName: toValue(options.productName),
|
|
@@ -81,15 +95,30 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
81
95
|
|
|
82
96
|
onMounted(() => {
|
|
83
97
|
void refreshProjects();
|
|
98
|
+
previewPollTimer = window.setInterval(refreshPreviewing, 2000);
|
|
84
99
|
});
|
|
85
100
|
|
|
101
|
+
onUnmounted(() => {
|
|
102
|
+
if (previewPollTimer !== undefined) window.clearInterval(previewPollTimer);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
watch(
|
|
106
|
+
() => toValue(options.panelRevision),
|
|
107
|
+
(revision) => {
|
|
108
|
+
if (!pendingRevisionSync) return;
|
|
109
|
+
syncedPanelRevision = revision;
|
|
110
|
+
pendingRevisionSync = false;
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
86
114
|
const dirty = computed(() => {
|
|
87
115
|
if (!activeProjectId.value || !syncedSnapshotRef.value) return false;
|
|
88
|
-
|
|
116
|
+
if (pendingRevisionSync) return false;
|
|
89
117
|
void toValue(options.blueprintDocument);
|
|
90
118
|
void toValue(options.blueprintMeta);
|
|
91
119
|
void toValue(options.productName);
|
|
92
120
|
void toValue(options.titleIconDataUrl);
|
|
121
|
+
if (toValue(options.panelRevision) !== syncedPanelRevision) return true;
|
|
93
122
|
return !workspaceSnapshotsEqual(buildCurrentSnapshot(), syncedSnapshotRef.value);
|
|
94
123
|
});
|
|
95
124
|
|
|
@@ -109,12 +138,13 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
109
138
|
activeProjectId.value = record.id;
|
|
110
139
|
activeProjectName.value = record.name;
|
|
111
140
|
syncedSnapshotRef.value = cloneWorkspaceSnapshot({
|
|
112
|
-
panelState: record.panelState,
|
|
113
141
|
blueprintDocument,
|
|
114
142
|
blueprintMeta,
|
|
115
143
|
productName: record.productName,
|
|
116
144
|
titleIconDataUrl: record.titleIconDataUrl ?? "",
|
|
117
145
|
});
|
|
146
|
+
syncedPanelRevision = toValue(options.panelRevision);
|
|
147
|
+
pendingRevisionSync = true;
|
|
118
148
|
options.onProjectApplied?.({
|
|
119
149
|
...record,
|
|
120
150
|
blueprintDocument,
|
|
@@ -127,7 +157,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
127
157
|
name: string;
|
|
128
158
|
createdAt?: number;
|
|
129
159
|
}) {
|
|
130
|
-
const snapshot = buildCurrentSnapshot();
|
|
160
|
+
const snapshot = cloneWorkspaceSnapshot(buildCurrentSnapshot());
|
|
131
161
|
const now = Date.now();
|
|
132
162
|
const id = persistOptions.id ?? createWorkspaceProjectId();
|
|
133
163
|
const createdAt = persistOptions.createdAt ?? now;
|
|
@@ -138,7 +168,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
138
168
|
name,
|
|
139
169
|
createdAt,
|
|
140
170
|
updatedAt: now,
|
|
141
|
-
panelState:
|
|
171
|
+
panelState: options.exportPanelData(),
|
|
142
172
|
blueprintDocument: snapshot.blueprintDocument,
|
|
143
173
|
blueprintMeta: snapshot.blueprintMeta,
|
|
144
174
|
productName: snapshot.productName,
|
|
@@ -150,81 +180,121 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
150
180
|
broadcastWorkspaceProjectUpdate(id, now);
|
|
151
181
|
activeProjectId.value = id;
|
|
152
182
|
activeProjectName.value = name;
|
|
153
|
-
syncedSnapshotRef.value =
|
|
183
|
+
syncedSnapshotRef.value = snapshot;
|
|
184
|
+
syncedPanelRevision = toValue(options.panelRevision);
|
|
185
|
+
pendingRevisionSync = false;
|
|
154
186
|
await refreshProjects();
|
|
155
187
|
return record;
|
|
156
188
|
}
|
|
157
189
|
|
|
158
190
|
async function handleCreateProject() {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
191
|
+
return runBusyTask(t("common.savingWorkspace"), async () => {
|
|
192
|
+
const name = resolveProjectName();
|
|
193
|
+
const record = await persistProject({ name });
|
|
194
|
+
return { name, id: record.id };
|
|
195
|
+
});
|
|
162
196
|
}
|
|
163
197
|
|
|
164
198
|
async function handleSaveProject() {
|
|
165
|
-
|
|
199
|
+
const projectId = activeProjectId.value;
|
|
200
|
+
if (!projectId) {
|
|
166
201
|
return handleCreateProject();
|
|
167
202
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
203
|
+
return runBusyTask(t("common.savingWorkspace"), async () => {
|
|
204
|
+
const existing = await getWorkspaceProject(projectId);
|
|
205
|
+
const name = resolveProjectName();
|
|
206
|
+
const record = await persistProject({
|
|
207
|
+
id: projectId,
|
|
208
|
+
name,
|
|
209
|
+
createdAt: existing?.createdAt,
|
|
210
|
+
});
|
|
211
|
+
return { name, id: record.id };
|
|
174
212
|
});
|
|
175
|
-
return { name, id: record.id };
|
|
176
213
|
}
|
|
177
214
|
|
|
178
215
|
async function handleOpenProject(id: string) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
216
|
+
await runBusyTask(t("common.loadingWorkspace"), async () => {
|
|
217
|
+
const record = await getWorkspaceProject(id);
|
|
218
|
+
if (!record) {
|
|
219
|
+
await refreshProjects();
|
|
220
|
+
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
221
|
+
}
|
|
222
|
+
applyProjectRecord(record);
|
|
223
|
+
});
|
|
185
224
|
}
|
|
186
225
|
|
|
187
226
|
async function handleSyncProject() {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
227
|
+
return runBusyTask(t("common.syncingWorkspace"), async () => {
|
|
228
|
+
if (!activeProjectId.value) {
|
|
229
|
+
throw new Error(t("panel.messages.saveWorkspaceFirst"));
|
|
230
|
+
}
|
|
231
|
+
const existing = await getWorkspaceProject(activeProjectId.value);
|
|
232
|
+
if (!existing) {
|
|
233
|
+
activeProjectId.value = null;
|
|
234
|
+
activeProjectName.value = null;
|
|
235
|
+
syncedSnapshotRef.value = null;
|
|
236
|
+
await refreshProjects();
|
|
237
|
+
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
238
|
+
}
|
|
239
|
+
const name = resolveProjectName() || existing.name;
|
|
240
|
+
await persistProject({
|
|
241
|
+
id: activeProjectId.value,
|
|
242
|
+
name,
|
|
243
|
+
createdAt: existing.createdAt,
|
|
244
|
+
});
|
|
245
|
+
return name;
|
|
204
246
|
});
|
|
205
|
-
return name;
|
|
206
247
|
}
|
|
207
248
|
|
|
208
249
|
async function handleDeleteProject(id: string) {
|
|
209
|
-
await
|
|
210
|
-
|
|
211
|
-
activeProjectId.value
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
250
|
+
await runBusyTask(t("common.deletingWorkspace"), async () => {
|
|
251
|
+
await deleteWorkspaceProject(id);
|
|
252
|
+
if (activeProjectId.value === id) {
|
|
253
|
+
activeProjectId.value = null;
|
|
254
|
+
activeProjectName.value = null;
|
|
255
|
+
syncedSnapshotRef.value = null;
|
|
256
|
+
}
|
|
257
|
+
previewWindows.delete(id);
|
|
258
|
+
refreshPreviewing();
|
|
259
|
+
await refreshProjects();
|
|
260
|
+
});
|
|
216
261
|
}
|
|
217
262
|
|
|
218
263
|
async function handlePreviewProject(id: string, previewOptions?: { syncFirst?: boolean }) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
264
|
+
await runBusyTask(t("common.openingPreview"), async () => {
|
|
265
|
+
if (previewOptions?.syncFirst && activeProjectId.value === id && dirty.value) {
|
|
266
|
+
await handleSyncProject();
|
|
267
|
+
}
|
|
268
|
+
const record =
|
|
269
|
+
(await getWorkspaceProject(id)) ?? readWorkspacePreviewCache(id);
|
|
270
|
+
if (record) {
|
|
271
|
+
writeWorkspacePreviewCache(record);
|
|
272
|
+
}
|
|
273
|
+
const existing = previewWindows.get(id);
|
|
274
|
+
if (existing && !existing.closed) {
|
|
275
|
+
try {
|
|
276
|
+
existing.focus();
|
|
277
|
+
} catch {
|
|
278
|
+
/* ignore */
|
|
279
|
+
}
|
|
280
|
+
refreshPreviewing();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const win = window.open(
|
|
284
|
+
buildOnlinePreviewUrl(id),
|
|
285
|
+
`abuilder-preview-${id}`
|
|
286
|
+
);
|
|
287
|
+
if (win) {
|
|
288
|
+
previewWindows.set(id, win);
|
|
289
|
+
try {
|
|
290
|
+
win.opener = null;
|
|
291
|
+
win.focus();
|
|
292
|
+
} catch {
|
|
293
|
+
/* ignore */
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
refreshPreviewing();
|
|
297
|
+
});
|
|
228
298
|
}
|
|
229
299
|
|
|
230
300
|
return {
|
|
@@ -232,6 +302,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
232
302
|
activeProjectId,
|
|
233
303
|
activeProjectName,
|
|
234
304
|
dirty,
|
|
305
|
+
previewingProjectIds,
|
|
235
306
|
handleCreateProject,
|
|
236
307
|
handleSaveProject,
|
|
237
308
|
handleOpenProject,
|
|
@@ -4,7 +4,10 @@ const CACHE_PREFIX = "arronqzy-workspace-preview:";
|
|
|
4
4
|
|
|
5
5
|
export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void {
|
|
6
6
|
try {
|
|
7
|
-
localStorage.setItem(
|
|
7
|
+
localStorage.setItem(
|
|
8
|
+
CACHE_PREFIX + record.id,
|
|
9
|
+
JSON.stringify({ id: record.id, updatedAt: record.updatedAt })
|
|
10
|
+
);
|
|
8
11
|
} catch {
|
|
9
12
|
// ignore quota / private mode
|
|
10
13
|
}
|
|
@@ -16,7 +19,9 @@ export function readWorkspacePreviewCache(
|
|
|
16
19
|
try {
|
|
17
20
|
const raw = localStorage.getItem(CACHE_PREFIX + projectId);
|
|
18
21
|
if (!raw) return null;
|
|
19
|
-
|
|
22
|
+
const parsed = JSON.parse(raw) as Partial<WorkspaceProjectRecord> | null;
|
|
23
|
+
if (!parsed || typeof parsed !== "object" || !parsed.panelState) return null;
|
|
24
|
+
return parsed as WorkspaceProjectRecord;
|
|
20
25
|
} catch {
|
|
21
26
|
return null;
|
|
22
27
|
}
|
|
@@ -77,17 +77,35 @@ export function createWorkspaceProjectId(): string {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[]> {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
const db = await openDb();
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
83
|
+
const store = tx.objectStore(STORE_NAME);
|
|
84
|
+
const request = store.openCursor();
|
|
85
|
+
const items: WorkspaceProjectListItem[] = [];
|
|
86
|
+
request.onerror = () =>
|
|
87
|
+
reject(request.error ?? new Error("indexeddb-request-failed"));
|
|
88
|
+
request.onsuccess = () => {
|
|
89
|
+
const cursor = request.result;
|
|
90
|
+
if (!cursor) return;
|
|
91
|
+
const record = cursor.value as WorkspaceProjectRecord;
|
|
92
|
+
items.push({
|
|
93
|
+
id: record.id,
|
|
94
|
+
name: record.name,
|
|
95
|
+
createdAt: record.createdAt,
|
|
96
|
+
updatedAt: record.updatedAt,
|
|
97
|
+
});
|
|
98
|
+
cursor.continue();
|
|
99
|
+
};
|
|
100
|
+
tx.oncomplete = () => {
|
|
101
|
+
db.close();
|
|
102
|
+
resolve(items.sort((a, b) => b.updatedAt - a.updatedAt));
|
|
103
|
+
};
|
|
104
|
+
tx.onerror = () => {
|
|
105
|
+
db.close();
|
|
106
|
+
reject(tx.error ?? new Error("indexeddb-transaction-failed"));
|
|
107
|
+
};
|
|
108
|
+
});
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
export async function getWorkspaceProject(
|
|
@@ -3,16 +3,25 @@ import {
|
|
|
3
3
|
type BlueprintDocument,
|
|
4
4
|
type BlueprintMetaDraft,
|
|
5
5
|
} from "@arronqzy/vue-blueprint";
|
|
6
|
-
import type { State } from "@arronqzy/rx-store";
|
|
7
6
|
|
|
8
7
|
export type WorkspaceSnapshot = {
|
|
9
|
-
panelState: State;
|
|
10
8
|
blueprintDocument: BlueprintDocument;
|
|
11
9
|
blueprintMeta: BlueprintMetaDraft;
|
|
12
10
|
productName: string;
|
|
13
11
|
titleIconDataUrl: string;
|
|
14
12
|
};
|
|
15
13
|
|
|
14
|
+
function cloneBlueprintDocument(document: BlueprintDocument): BlueprintDocument {
|
|
15
|
+
if (typeof structuredClone === "function") {
|
|
16
|
+
try {
|
|
17
|
+
return structuredClone(document);
|
|
18
|
+
} catch {
|
|
19
|
+
/* fall through */
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return JSON.parse(JSON.stringify(document)) as BlueprintDocument;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
export function workspaceSnapshotsEqual(
|
|
17
26
|
a: WorkspaceSnapshot,
|
|
18
27
|
b: WorkspaceSnapshot
|
|
@@ -22,9 +31,17 @@ export function workspaceSnapshotsEqual(
|
|
|
22
31
|
if (a.blueprintMeta.name !== b.blueprintMeta.name) return false;
|
|
23
32
|
if ((a.blueprintMeta.remark ?? "") !== (b.blueprintMeta.remark ?? "")) return false;
|
|
24
33
|
if (!blueprintDocumentsEqual(a.blueprintDocument, b.blueprintDocument)) return false;
|
|
25
|
-
return
|
|
34
|
+
return true;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
export function cloneWorkspaceSnapshot(snapshot: WorkspaceSnapshot): WorkspaceSnapshot {
|
|
29
|
-
return
|
|
38
|
+
return {
|
|
39
|
+
blueprintDocument: cloneBlueprintDocument(snapshot.blueprintDocument),
|
|
40
|
+
blueprintMeta: {
|
|
41
|
+
name: snapshot.blueprintMeta.name,
|
|
42
|
+
remark: snapshot.blueprintMeta.remark ?? "",
|
|
43
|
+
},
|
|
44
|
+
productName: snapshot.productName,
|
|
45
|
+
titleIconDataUrl: snapshot.titleIconDataUrl,
|
|
46
|
+
};
|
|
30
47
|
}
|
|
@@ -28,6 +28,14 @@ export function clearViewElementScopes() {
|
|
|
28
28
|
emit();
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function removeViewElementScopes(elementIds: readonly string[]) {
|
|
32
|
+
let changed = false;
|
|
33
|
+
for (const id of elementIds) {
|
|
34
|
+
if (scopes.delete(id)) changed = true;
|
|
35
|
+
}
|
|
36
|
+
if (changed) emit();
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
export function setViewElementScope(elementId: string, scope: unknown) {
|
|
32
40
|
scopes.set(elementId, scope);
|
|
33
41
|
emit();
|
package/src/panel/types.ts
CHANGED
|
@@ -56,6 +56,7 @@ export type ReferenceCopyMode = "shallow" | "deep";
|
|
|
56
56
|
|
|
57
57
|
export type { PanelTableConfig } from "@arronqzy/view-table";
|
|
58
58
|
import type { PanelTableConfig } from "@arronqzy/view-table";
|
|
59
|
+
import type { Scene3dConfig } from "@arronqzy/view-scene3d";
|
|
59
60
|
|
|
60
61
|
export type PanelElement = {
|
|
61
62
|
id: string;
|
|
@@ -75,8 +76,8 @@ export type PanelElement = {
|
|
|
75
76
|
textFontSize?: number;
|
|
76
77
|
textFontWeight?: string;
|
|
77
78
|
textColor?: string;
|
|
78
|
-
textLineHeight?: number;
|
|
79
|
-
textAlign?: "left" | "center" | "right" | "justify";
|
|
79
|
+
textLineHeight?: number | "auto";
|
|
80
|
+
textAlign?: "left" | "center" | "right" | "justify" | "middle";
|
|
80
81
|
gridRows?: number;
|
|
81
82
|
gridCols?: number;
|
|
82
83
|
gridGap?: number;
|
|
@@ -101,6 +102,7 @@ export type PanelElement = {
|
|
|
101
102
|
geometryColor?: string;
|
|
102
103
|
geometryScript?: string;
|
|
103
104
|
geometrySketchDataUrl?: string;
|
|
105
|
+
scene3d?: Scene3dConfig;
|
|
104
106
|
style?: PanelElementStyle;
|
|
105
107
|
x: number;
|
|
106
108
|
y: number;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { shallowRef } from "vue";
|
|
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
|
+
export const busyOverlayMessage = shallowRef<string | null>(null);
|
|
17
|
+
let busyDepth = 0;
|
|
18
|
+
|
|
19
|
+
function setBusyMessage(message: string | null) {
|
|
20
|
+
busyOverlayMessage.value = message;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function yieldToPaint(): Promise<void> {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
26
|
+
setTimeout(resolve, 0);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
requestAnimationFrame(() => {
|
|
30
|
+
requestAnimationFrame(() => resolve());
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runBusyTask<T>(
|
|
36
|
+
message: string,
|
|
37
|
+
task: () => Promise<T> | T
|
|
38
|
+
): Promise<T> {
|
|
39
|
+
const previous = busyOverlayMessage.value;
|
|
40
|
+
busyDepth += 1;
|
|
41
|
+
setBusyMessage(message);
|
|
42
|
+
await yieldToPaint();
|
|
43
|
+
try {
|
|
44
|
+
return await task();
|
|
45
|
+
} finally {
|
|
46
|
+
busyDepth = Math.max(0, busyDepth - 1);
|
|
47
|
+
setBusyMessage(busyDepth === 0 ? null : previous);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatFileSize(bytes: number) {
|
|
52
|
+
if (bytes >= 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
53
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function assertFileSize(file: Blob, kind: MediaFileKind) {
|
|
57
|
+
const limit = FILE_SIZE_LIMITS[kind];
|
|
58
|
+
if (file.size <= limit) return;
|
|
59
|
+
const t = tForLocale(resolveLocale());
|
|
60
|
+
throw new Error(t("common.fileTooLarge", { max: formatFileSize(limit) }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function readFileAsDataUrl(
|
|
64
|
+
file: Blob,
|
|
65
|
+
errorMessage: string,
|
|
66
|
+
kind: MediaFileKind = "image"
|
|
67
|
+
): Promise<string> {
|
|
68
|
+
assertFileSize(file, kind);
|
|
69
|
+
const t = tForLocale(resolveLocale());
|
|
70
|
+
return runBusyTask(t("common.uploadingFile"), () => {
|
|
71
|
+
return new Promise<string>((resolve, reject) => {
|
|
72
|
+
const reader = new FileReader();
|
|
73
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
74
|
+
reader.onerror = () => reject(new Error(errorMessage));
|
|
75
|
+
reader.readAsDataURL(file);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let jsonWorker: Worker | null = null;
|
|
81
|
+
let jsonWorkerObjectUrl: string | null = null;
|
|
82
|
+
let jsonRequestId = 0;
|
|
83
|
+
const jsonPending = new Map<
|
|
84
|
+
number,
|
|
85
|
+
{ resolve: (value: unknown) => void; reject: (error: Error) => void }
|
|
86
|
+
>();
|
|
87
|
+
|
|
88
|
+
function ensureJsonWorker(): Worker | null {
|
|
89
|
+
if (typeof Worker === "undefined") return null;
|
|
90
|
+
if (jsonWorker) return jsonWorker;
|
|
91
|
+
const source = `
|
|
92
|
+
self.onmessage = (event) => {
|
|
93
|
+
const { id, text } = event.data || {};
|
|
94
|
+
try {
|
|
95
|
+
self.postMessage({ id, ok: true, value: JSON.parse(text) });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
self.postMessage({
|
|
98
|
+
id,
|
|
99
|
+
ok: false,
|
|
100
|
+
error: error instanceof Error ? error.message : String(error),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
`;
|
|
105
|
+
jsonWorkerObjectUrl = URL.createObjectURL(
|
|
106
|
+
new Blob([source], { type: "text/javascript" })
|
|
107
|
+
);
|
|
108
|
+
jsonWorker = new Worker(jsonWorkerObjectUrl);
|
|
109
|
+
jsonWorker.onmessage = (event: MessageEvent) => {
|
|
110
|
+
const { id, ok, value, error } = event.data ?? {};
|
|
111
|
+
const pending = jsonPending.get(id);
|
|
112
|
+
if (!pending) return;
|
|
113
|
+
jsonPending.delete(id);
|
|
114
|
+
if (ok) pending.resolve(value);
|
|
115
|
+
else pending.reject(new Error(String(error ?? "json-parse-failed")));
|
|
116
|
+
};
|
|
117
|
+
jsonWorker.onerror = () => {
|
|
118
|
+
jsonPending.forEach(({ reject }) => reject(new Error("json-worker-failed")));
|
|
119
|
+
jsonPending.clear();
|
|
120
|
+
disposeJsonWorker();
|
|
121
|
+
};
|
|
122
|
+
if (typeof window !== "undefined") {
|
|
123
|
+
window.addEventListener("pagehide", disposeJsonWorker, { once: true });
|
|
124
|
+
}
|
|
125
|
+
return jsonWorker;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function disposeJsonWorker() {
|
|
129
|
+
jsonPending.forEach(({ reject }) => reject(new Error("json-worker-disposed")));
|
|
130
|
+
jsonPending.clear();
|
|
131
|
+
jsonWorker?.terminate();
|
|
132
|
+
jsonWorker = null;
|
|
133
|
+
if (jsonWorkerObjectUrl) {
|
|
134
|
+
URL.revokeObjectURL(jsonWorkerObjectUrl);
|
|
135
|
+
jsonWorkerObjectUrl = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function parseJsonText<T>(text: string): Promise<T> {
|
|
140
|
+
if (text.length < JSON_WORKER_THRESHOLD) {
|
|
141
|
+
return JSON.parse(text) as T;
|
|
142
|
+
}
|
|
143
|
+
const worker = ensureJsonWorker();
|
|
144
|
+
if (!worker) return JSON.parse(text) as T;
|
|
145
|
+
const id = ++jsonRequestId;
|
|
146
|
+
return new Promise<T>((resolve, reject) => {
|
|
147
|
+
jsonPending.set(id, {
|
|
148
|
+
resolve: (value) => resolve(value as T),
|
|
149
|
+
reject,
|
|
150
|
+
});
|
|
151
|
+
worker.postMessage({ id, text });
|
|
152
|
+
});
|
|
153
|
+
}
|