@arronqzy/react-view 0.0.1
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/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/pnpm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
- package/.turbo/turbo-lint.log +4 -0
- package/dist/styles.css +2624 -0
- package/env.d.ts +11 -0
- package/operation-guide.md +275 -0
- package/package.json +43 -0
- package/postcss.config.cjs +7 -0
- package/readme.md +21 -0
- package/src/core/createAction.ts +5 -0
- package/src/core/createView.ts +28 -0
- package/src/core/instructions.tsx +57 -0
- package/src/core/nodePool.ts +127 -0
- package/src/core/signal.ts +130 -0
- package/src/core/template.tsx +25 -0
- package/src/index.ts +7 -0
- package/src/panel/ReactViewOnlinePreview.tsx +331 -0
- package/src/panel/ReactViewPanel.tsx +3221 -0
- package/src/panel/components/ElementsLayer.tsx +891 -0
- package/src/panel/components/MaterialSidebar.tsx +1142 -0
- package/src/panel/components/MoveableLayer.tsx +803 -0
- package/src/panel/components/PanelCanvas.tsx +660 -0
- package/src/panel/components/PanelConfigSidebar.tsx +3703 -0
- package/src/panel/components/PanelRulers.tsx +89 -0
- package/src/panel/components/SelectLayer.tsx +106 -0
- package/src/panel/components/ViewElementScopePanel.tsx +79 -0
- package/src/panel/components/WorkspaceConfigSidebar.tsx +158 -0
- package/src/panel/components/WorkspaceProjectNav.tsx +220 -0
- package/src/panel/components/WorkspaceStageSplit.tsx +179 -0
- package/src/panel/components/scope-config/ScopeConfigContext.tsx +170 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +250 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +59 -0
- package/src/panel/constants/messages.ts +34 -0
- package/src/panel/constants/zIndex.ts +6 -0
- package/src/panel/hooks/usePanelElements.ts +1108 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +278 -0
- package/src/panel/index.tsx +3 -0
- package/src/panel/library/workspace-project-cache.ts +23 -0
- package/src/panel/library/workspace-project-db.ts +112 -0
- package/src/panel/library/workspace-project-sync.ts +41 -0
- package/src/panel/library/workspace-snapshot.ts +27 -0
- package/src/panel/scope/view-scope-store.ts +55 -0
- package/src/panel/types.ts +125 -0
- package/src/panel/utils/chartOptionBuilder.ts +316 -0
- package/src/panel/utils/gridPlacement.ts +189 -0
- package/src/panel/utils/mappingLayerOps.ts +142 -0
- package/src/panel/utils/panelElementDefaults.ts +161 -0
- package/src/panel/utils/panelElementNodes.ts +35 -0
- package/src/panel/utils/panelStateIO.ts +124 -0
- package/src/panel/utils/scope-autocomplete.ts +114 -0
- package/src/panel/utils/scope-field-labels.ts +45 -0
- package/src/panel/utils/scope-template-warnings.ts +135 -0
- package/src/panel/utils/scope-template.ts +75 -0
- package/src/panel/utils/updateElementDraft.ts +221 -0
- package/src/panel/viewportZoom.ts +26 -0
- package/src/tailwind.css +58 -0
- package/src/types/react-infinite-viewer.d.ts +39 -0
- package/tailwind.config.ts +7 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { BlueprintGraph } from "@arronqzy/react-blueprint";
|
|
3
|
+
import type { BlueprintDocument, BlueprintMetaDraft } from "@arronqzy/react-blueprint";
|
|
4
|
+
import type { State } from "../../../../rx-store/src/types";
|
|
5
|
+
import {
|
|
6
|
+
createWorkspaceProjectId,
|
|
7
|
+
deleteWorkspaceProject,
|
|
8
|
+
getWorkspaceProject,
|
|
9
|
+
listWorkspaceProjects,
|
|
10
|
+
putWorkspaceProject,
|
|
11
|
+
type WorkspaceProjectListItem,
|
|
12
|
+
type WorkspaceProjectRecord,
|
|
13
|
+
} from "../library/workspace-project-db";
|
|
14
|
+
import { broadcastWorkspaceProjectUpdate } from "../library/workspace-project-sync";
|
|
15
|
+
import { writeWorkspacePreviewCache, readWorkspacePreviewCache } from "../library/workspace-project-cache";
|
|
16
|
+
import {
|
|
17
|
+
cloneWorkspaceSnapshot,
|
|
18
|
+
workspaceSnapshotsEqual,
|
|
19
|
+
type WorkspaceSnapshot,
|
|
20
|
+
} from "../library/workspace-snapshot";
|
|
21
|
+
|
|
22
|
+
export type UseWorkspaceProjectsOptions = {
|
|
23
|
+
exportPanelData: () => State;
|
|
24
|
+
importPanelData: (state: State) => boolean;
|
|
25
|
+
blueprintDocument: BlueprintDocument;
|
|
26
|
+
blueprintMeta: BlueprintMetaDraft;
|
|
27
|
+
setBlueprintGraph: (graph: ReturnType<typeof BlueprintGraph.fromDocument>) => void;
|
|
28
|
+
setBlueprintMeta: (meta: BlueprintMetaDraft) => void;
|
|
29
|
+
productName: string;
|
|
30
|
+
setProductName: (name: string) => void;
|
|
31
|
+
titleIconDataUrl: string;
|
|
32
|
+
setTitleIconDataUrl: (url: string) => void;
|
|
33
|
+
/** 视图面板修订号,用于检测未同步修改 */
|
|
34
|
+
panelRevision: unknown;
|
|
35
|
+
/** 工作区记录应用到编辑器后的回调(用于恢复蓝图面板等 UI 状态) */
|
|
36
|
+
onProjectApplied?: (record: WorkspaceProjectRecord) => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function buildOnlinePreviewUrl(projectId: string): string {
|
|
40
|
+
const url = new URL(window.location.href);
|
|
41
|
+
url.search = "";
|
|
42
|
+
url.searchParams.set("preview", "online");
|
|
43
|
+
url.searchParams.set("projectId", projectId);
|
|
44
|
+
const previewInstanceId =
|
|
45
|
+
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
46
|
+
? crypto.randomUUID()
|
|
47
|
+
: `pv-${Date.now()}`;
|
|
48
|
+
url.searchParams.set("pid", previewInstanceId);
|
|
49
|
+
return url.toString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useWorkspaceProjects({
|
|
53
|
+
exportPanelData,
|
|
54
|
+
importPanelData,
|
|
55
|
+
blueprintDocument,
|
|
56
|
+
blueprintMeta,
|
|
57
|
+
setBlueprintGraph,
|
|
58
|
+
setBlueprintMeta,
|
|
59
|
+
productName,
|
|
60
|
+
setProductName,
|
|
61
|
+
titleIconDataUrl,
|
|
62
|
+
setTitleIconDataUrl,
|
|
63
|
+
panelRevision,
|
|
64
|
+
onProjectApplied,
|
|
65
|
+
}: UseWorkspaceProjectsOptions) {
|
|
66
|
+
const [projects, setProjects] = useState<WorkspaceProjectListItem[]>([]);
|
|
67
|
+
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
|
68
|
+
const [activeProjectName, setActiveProjectName] = useState<string | null>(null);
|
|
69
|
+
const syncedSnapshotRef = useRef<WorkspaceSnapshot | null>(null);
|
|
70
|
+
|
|
71
|
+
const resolveProjectName = useCallback(() => {
|
|
72
|
+
return productName.trim() || "未命名产物";
|
|
73
|
+
}, [productName]);
|
|
74
|
+
|
|
75
|
+
const buildCurrentSnapshot = useCallback((): WorkspaceSnapshot => {
|
|
76
|
+
return {
|
|
77
|
+
panelState: exportPanelData(),
|
|
78
|
+
blueprintDocument,
|
|
79
|
+
blueprintMeta: { ...blueprintMeta },
|
|
80
|
+
productName,
|
|
81
|
+
titleIconDataUrl,
|
|
82
|
+
};
|
|
83
|
+
}, [
|
|
84
|
+
blueprintDocument,
|
|
85
|
+
blueprintMeta,
|
|
86
|
+
exportPanelData,
|
|
87
|
+
productName,
|
|
88
|
+
titleIconDataUrl,
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const refreshProjects = useCallback(async () => {
|
|
92
|
+
const items = await listWorkspaceProjects();
|
|
93
|
+
setProjects(items);
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
void refreshProjects();
|
|
98
|
+
}, [refreshProjects]);
|
|
99
|
+
|
|
100
|
+
const dirty = useMemo(() => {
|
|
101
|
+
if (!activeProjectId || !syncedSnapshotRef.current) return false;
|
|
102
|
+
return !workspaceSnapshotsEqual(buildCurrentSnapshot(), syncedSnapshotRef.current);
|
|
103
|
+
}, [
|
|
104
|
+
activeProjectId,
|
|
105
|
+
blueprintDocument,
|
|
106
|
+
blueprintMeta,
|
|
107
|
+
productName,
|
|
108
|
+
titleIconDataUrl,
|
|
109
|
+
panelRevision,
|
|
110
|
+
buildCurrentSnapshot,
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const applyProjectRecord = useCallback(
|
|
114
|
+
(record: WorkspaceProjectRecord) => {
|
|
115
|
+
const blueprintDocument =
|
|
116
|
+
record.blueprintDocument ?? BlueprintGraph.empty().document;
|
|
117
|
+
const blueprintMeta: BlueprintMetaDraft = {
|
|
118
|
+
name: record.blueprintMeta?.name ?? "未命名蓝图",
|
|
119
|
+
remark: record.blueprintMeta?.remark ?? "",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
importPanelData(record.panelState);
|
|
123
|
+
setBlueprintGraph(BlueprintGraph.fromDocument(blueprintDocument));
|
|
124
|
+
setBlueprintMeta(blueprintMeta);
|
|
125
|
+
setProductName(record.productName);
|
|
126
|
+
setTitleIconDataUrl(record.titleIconDataUrl ?? "");
|
|
127
|
+
setActiveProjectId(record.id);
|
|
128
|
+
setActiveProjectName(record.name);
|
|
129
|
+
syncedSnapshotRef.current = cloneWorkspaceSnapshot({
|
|
130
|
+
panelState: record.panelState,
|
|
131
|
+
blueprintDocument,
|
|
132
|
+
blueprintMeta,
|
|
133
|
+
productName: record.productName,
|
|
134
|
+
titleIconDataUrl: record.titleIconDataUrl ?? "",
|
|
135
|
+
});
|
|
136
|
+
onProjectApplied?.({
|
|
137
|
+
...record,
|
|
138
|
+
blueprintDocument,
|
|
139
|
+
blueprintMeta,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
[
|
|
143
|
+
importPanelData,
|
|
144
|
+
onProjectApplied,
|
|
145
|
+
setBlueprintGraph,
|
|
146
|
+
setBlueprintMeta,
|
|
147
|
+
setProductName,
|
|
148
|
+
setTitleIconDataUrl,
|
|
149
|
+
]
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const persistProject = useCallback(
|
|
153
|
+
async (options: { id?: string; name: string; createdAt?: number }) => {
|
|
154
|
+
const snapshot = buildCurrentSnapshot();
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
const id = options.id ?? createWorkspaceProjectId();
|
|
157
|
+
const createdAt = options.createdAt ?? now;
|
|
158
|
+
const name = options.name.trim() || resolveProjectName();
|
|
159
|
+
|
|
160
|
+
const record: WorkspaceProjectRecord = {
|
|
161
|
+
id,
|
|
162
|
+
name,
|
|
163
|
+
createdAt,
|
|
164
|
+
updatedAt: now,
|
|
165
|
+
panelState: snapshot.panelState,
|
|
166
|
+
blueprintDocument: snapshot.blueprintDocument,
|
|
167
|
+
blueprintMeta: snapshot.blueprintMeta,
|
|
168
|
+
productName: snapshot.productName,
|
|
169
|
+
titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
await putWorkspaceProject(record);
|
|
173
|
+
writeWorkspacePreviewCache(record);
|
|
174
|
+
broadcastWorkspaceProjectUpdate(id, now);
|
|
175
|
+
setActiveProjectId(id);
|
|
176
|
+
setActiveProjectName(name);
|
|
177
|
+
syncedSnapshotRef.current = cloneWorkspaceSnapshot(snapshot);
|
|
178
|
+
await refreshProjects();
|
|
179
|
+
return record;
|
|
180
|
+
},
|
|
181
|
+
[buildCurrentSnapshot, resolveProjectName, refreshProjects]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const handleCreateProject = useCallback(async () => {
|
|
185
|
+
const name = resolveProjectName();
|
|
186
|
+
const record = await persistProject({ name });
|
|
187
|
+
return { name, id: record.id };
|
|
188
|
+
}, [persistProject, resolveProjectName]);
|
|
189
|
+
|
|
190
|
+
const handleSaveProject = useCallback(async () => {
|
|
191
|
+
if (!activeProjectId) {
|
|
192
|
+
return handleCreateProject();
|
|
193
|
+
}
|
|
194
|
+
const existing = await getWorkspaceProject(activeProjectId);
|
|
195
|
+
const name = resolveProjectName();
|
|
196
|
+
const record = await persistProject({
|
|
197
|
+
id: activeProjectId,
|
|
198
|
+
name,
|
|
199
|
+
createdAt: existing?.createdAt,
|
|
200
|
+
});
|
|
201
|
+
return { name, id: record.id };
|
|
202
|
+
}, [activeProjectId, handleCreateProject, persistProject, resolveProjectName]);
|
|
203
|
+
|
|
204
|
+
const handleOpenProject = useCallback(
|
|
205
|
+
async (id: string) => {
|
|
206
|
+
const record = await getWorkspaceProject(id);
|
|
207
|
+
if (!record) {
|
|
208
|
+
await refreshProjects();
|
|
209
|
+
throw new Error("工作区不存在或已被删除");
|
|
210
|
+
}
|
|
211
|
+
applyProjectRecord(record);
|
|
212
|
+
},
|
|
213
|
+
[applyProjectRecord, refreshProjects]
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const handleSyncProject = useCallback(async () => {
|
|
217
|
+
if (!activeProjectId) {
|
|
218
|
+
throw new Error("请先保存工作区");
|
|
219
|
+
}
|
|
220
|
+
const existing = await getWorkspaceProject(activeProjectId);
|
|
221
|
+
if (!existing) {
|
|
222
|
+
setActiveProjectId(null);
|
|
223
|
+
setActiveProjectName(null);
|
|
224
|
+
syncedSnapshotRef.current = null;
|
|
225
|
+
await refreshProjects();
|
|
226
|
+
throw new Error("工作区不存在或已被删除");
|
|
227
|
+
}
|
|
228
|
+
const name = resolveProjectName() || existing.name;
|
|
229
|
+
await persistProject({
|
|
230
|
+
id: activeProjectId,
|
|
231
|
+
name,
|
|
232
|
+
createdAt: existing.createdAt,
|
|
233
|
+
});
|
|
234
|
+
return name;
|
|
235
|
+
}, [activeProjectId, persistProject, refreshProjects, resolveProjectName]);
|
|
236
|
+
|
|
237
|
+
const handleDeleteProject = useCallback(
|
|
238
|
+
async (id: string) => {
|
|
239
|
+
await deleteWorkspaceProject(id);
|
|
240
|
+
if (activeProjectId === id) {
|
|
241
|
+
setActiveProjectId(null);
|
|
242
|
+
setActiveProjectName(null);
|
|
243
|
+
syncedSnapshotRef.current = null;
|
|
244
|
+
}
|
|
245
|
+
await refreshProjects();
|
|
246
|
+
},
|
|
247
|
+
[activeProjectId, refreshProjects]
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const handlePreviewProject = useCallback(
|
|
251
|
+
async (id: string, options?: { syncFirst?: boolean }) => {
|
|
252
|
+
if (options?.syncFirst && activeProjectId === id && dirty) {
|
|
253
|
+
await handleSyncProject();
|
|
254
|
+
}
|
|
255
|
+
const record =
|
|
256
|
+
(await getWorkspaceProject(id)) ?? readWorkspacePreviewCache(id);
|
|
257
|
+
if (record) {
|
|
258
|
+
writeWorkspacePreviewCache(record);
|
|
259
|
+
}
|
|
260
|
+
window.open(buildOnlinePreviewUrl(id), "_blank", "noopener,noreferrer");
|
|
261
|
+
},
|
|
262
|
+
[activeProjectId, dirty, handleSyncProject]
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
projects,
|
|
267
|
+
activeProjectId,
|
|
268
|
+
activeProjectName,
|
|
269
|
+
dirty,
|
|
270
|
+
handleCreateProject,
|
|
271
|
+
handleSaveProject,
|
|
272
|
+
handleOpenProject,
|
|
273
|
+
handleSyncProject,
|
|
274
|
+
handleDeleteProject,
|
|
275
|
+
handlePreviewProject,
|
|
276
|
+
refreshProjects,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WorkspaceProjectRecord } from "./workspace-project-db";
|
|
2
|
+
|
|
3
|
+
const CACHE_PREFIX = "arronqzy-workspace-preview:";
|
|
4
|
+
|
|
5
|
+
export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void {
|
|
6
|
+
try {
|
|
7
|
+
localStorage.setItem(CACHE_PREFIX + record.id, JSON.stringify(record));
|
|
8
|
+
} catch {
|
|
9
|
+
// ignore quota / private mode
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readWorkspacePreviewCache(
|
|
14
|
+
projectId: string
|
|
15
|
+
): WorkspaceProjectRecord | null {
|
|
16
|
+
try {
|
|
17
|
+
const raw = localStorage.getItem(CACHE_PREFIX + projectId);
|
|
18
|
+
if (!raw) return null;
|
|
19
|
+
return JSON.parse(raw) as WorkspaceProjectRecord;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { BlueprintDocument } from "@arronqzy/react-blueprint";
|
|
2
|
+
import type { BlueprintMetaDraft } from "@arronqzy/react-blueprint";
|
|
3
|
+
import type { State } from "../../../../rx-store/src/types";
|
|
4
|
+
|
|
5
|
+
const DB_NAME = "arronqzy-workspace-projects";
|
|
6
|
+
const DB_VERSION = 1;
|
|
7
|
+
const STORE_NAME = "projects";
|
|
8
|
+
|
|
9
|
+
export type WorkspaceProjectListItem = {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
updatedAt: number;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type WorkspaceProjectRecord = {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
updatedAt: number;
|
|
21
|
+
panelState: State;
|
|
22
|
+
blueprintDocument: BlueprintDocument;
|
|
23
|
+
blueprintMeta: BlueprintMetaDraft;
|
|
24
|
+
productName: string;
|
|
25
|
+
titleIconDataUrl?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function openDb(): Promise<IDBDatabase> {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
31
|
+
request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
|
|
32
|
+
request.onupgradeneeded = () => {
|
|
33
|
+
const db = request.result;
|
|
34
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
35
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
36
|
+
store.createIndex("updatedAt", "updatedAt", { unique: false });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
request.onsuccess = () => resolve(request.result);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runTransaction<T>(
|
|
44
|
+
mode: IDBTransactionMode,
|
|
45
|
+
runner: (store: IDBObjectStore) => IDBRequest<T>
|
|
46
|
+
): Promise<T> {
|
|
47
|
+
return openDb().then(
|
|
48
|
+
(db) =>
|
|
49
|
+
new Promise<T>((resolve, reject) => {
|
|
50
|
+
const tx = db.transaction(STORE_NAME, mode);
|
|
51
|
+
const store = tx.objectStore(STORE_NAME);
|
|
52
|
+
const request = runner(store);
|
|
53
|
+
let result!: T;
|
|
54
|
+
|
|
55
|
+
request.onerror = () =>
|
|
56
|
+
reject(request.error ?? new Error("indexeddb-request-failed"));
|
|
57
|
+
request.onsuccess = () => {
|
|
58
|
+
result = request.result as T;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
tx.oncomplete = () => {
|
|
62
|
+
db.close();
|
|
63
|
+
resolve(result);
|
|
64
|
+
};
|
|
65
|
+
tx.onerror = () => {
|
|
66
|
+
db.close();
|
|
67
|
+
reject(tx.error ?? new Error("indexeddb-transaction-failed"));
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createWorkspaceProjectId(): string {
|
|
74
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
75
|
+
return crypto.randomUUID();
|
|
76
|
+
}
|
|
77
|
+
return `ws-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[]> {
|
|
81
|
+
const records = await runTransaction<WorkspaceProjectRecord[]>("readonly", (store) =>
|
|
82
|
+
store.getAll()
|
|
83
|
+
);
|
|
84
|
+
return records
|
|
85
|
+
.map((record) => ({
|
|
86
|
+
id: record.id,
|
|
87
|
+
name: record.name,
|
|
88
|
+
createdAt: record.createdAt,
|
|
89
|
+
updatedAt: record.updatedAt,
|
|
90
|
+
}))
|
|
91
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function getWorkspaceProject(
|
|
95
|
+
id: string
|
|
96
|
+
): Promise<WorkspaceProjectRecord | null> {
|
|
97
|
+
const record = await runTransaction<WorkspaceProjectRecord | undefined>("readonly", (store) =>
|
|
98
|
+
store.get(id)
|
|
99
|
+
);
|
|
100
|
+
return record ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function putWorkspaceProject(
|
|
104
|
+
record: WorkspaceProjectRecord
|
|
105
|
+
): Promise<WorkspaceProjectRecord> {
|
|
106
|
+
await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record));
|
|
107
|
+
return record;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function deleteWorkspaceProject(id: string): Promise<void> {
|
|
111
|
+
await runTransaction<undefined>("readwrite", (store) => store.delete(id));
|
|
112
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const CHANNEL_PREFIX = "arronqzy-workspace-project";
|
|
2
|
+
|
|
3
|
+
export type WorkspaceProjectSyncMessage = {
|
|
4
|
+
type: "updated";
|
|
5
|
+
projectId: string;
|
|
6
|
+
updatedAt: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function getWorkspaceProjectChannelName(projectId: string): string {
|
|
10
|
+
return `${CHANNEL_PREFIX}:${projectId}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function broadcastWorkspaceProjectUpdate(
|
|
14
|
+
projectId: string,
|
|
15
|
+
updatedAt: number
|
|
16
|
+
): void {
|
|
17
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
18
|
+
const channel = new BroadcastChannel(getWorkspaceProjectChannelName(projectId));
|
|
19
|
+
channel.postMessage({
|
|
20
|
+
type: "updated",
|
|
21
|
+
projectId,
|
|
22
|
+
updatedAt,
|
|
23
|
+
} satisfies WorkspaceProjectSyncMessage);
|
|
24
|
+
channel.close();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function subscribeWorkspaceProjectUpdates(
|
|
28
|
+
projectId: string,
|
|
29
|
+
onUpdate: (message: WorkspaceProjectSyncMessage) => void
|
|
30
|
+
): () => void {
|
|
31
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
32
|
+
return () => {};
|
|
33
|
+
}
|
|
34
|
+
const channel = new BroadcastChannel(getWorkspaceProjectChannelName(projectId));
|
|
35
|
+
channel.onmessage = (event: MessageEvent<WorkspaceProjectSyncMessage>) => {
|
|
36
|
+
const data = event.data;
|
|
37
|
+
if (!data || data.type !== "updated" || data.projectId !== projectId) return;
|
|
38
|
+
onUpdate(data);
|
|
39
|
+
};
|
|
40
|
+
return () => channel.close();
|
|
41
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { blueprintDocumentsEqual } from "@arronqzy/react-blueprint";
|
|
2
|
+
import type { BlueprintDocument, BlueprintMetaDraft } from "@arronqzy/react-blueprint";
|
|
3
|
+
import type { State } from "../../../../rx-store/src/types";
|
|
4
|
+
|
|
5
|
+
export type WorkspaceSnapshot = {
|
|
6
|
+
panelState: State;
|
|
7
|
+
blueprintDocument: BlueprintDocument;
|
|
8
|
+
blueprintMeta: BlueprintMetaDraft;
|
|
9
|
+
productName: string;
|
|
10
|
+
titleIconDataUrl: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function workspaceSnapshotsEqual(
|
|
14
|
+
a: WorkspaceSnapshot,
|
|
15
|
+
b: WorkspaceSnapshot
|
|
16
|
+
): boolean {
|
|
17
|
+
if (a.productName !== b.productName) return false;
|
|
18
|
+
if (a.titleIconDataUrl !== b.titleIconDataUrl) return false;
|
|
19
|
+
if (a.blueprintMeta.name !== b.blueprintMeta.name) return false;
|
|
20
|
+
if ((a.blueprintMeta.remark ?? "") !== (b.blueprintMeta.remark ?? "")) return false;
|
|
21
|
+
if (!blueprintDocumentsEqual(a.blueprintDocument, b.blueprintDocument)) return false;
|
|
22
|
+
return JSON.stringify(a.panelState) === JSON.stringify(b.panelState);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function cloneWorkspaceSnapshot(snapshot: WorkspaceSnapshot): WorkspaceSnapshot {
|
|
26
|
+
return JSON.parse(JSON.stringify(snapshot)) as WorkspaceSnapshot;
|
|
27
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
|
|
3
|
+
const scopes = new Map<string, unknown>();
|
|
4
|
+
let version = 0;
|
|
5
|
+
const listeners = new Set<() => void>();
|
|
6
|
+
|
|
7
|
+
function emit() {
|
|
8
|
+
version += 1;
|
|
9
|
+
listeners.forEach((listener) => listener());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function subscribe(listener: () => void) {
|
|
13
|
+
listeners.add(listener);
|
|
14
|
+
return () => listeners.delete(listener);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function clearViewElementScopes() {
|
|
18
|
+
if (scopes.size === 0) return;
|
|
19
|
+
scopes.clear();
|
|
20
|
+
emit();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function setViewElementScope(elementId: string, scope: unknown) {
|
|
24
|
+
scopes.set(elementId, scope);
|
|
25
|
+
emit();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function setViewElementScopes(
|
|
29
|
+
elementIds: string[],
|
|
30
|
+
scope: unknown
|
|
31
|
+
) {
|
|
32
|
+
if (elementIds.length === 0) return;
|
|
33
|
+
elementIds.forEach((id) => scopes.set(id, scope));
|
|
34
|
+
emit();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getViewElementScope(elementId: string): unknown | undefined {
|
|
38
|
+
return scopes.get(elementId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function hasViewElementScope(elementId: string): boolean {
|
|
42
|
+
return scopes.has(elementId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useViewElementScope(elementId: string | null | undefined) {
|
|
46
|
+
return useSyncExternalStore(
|
|
47
|
+
subscribe,
|
|
48
|
+
() => (elementId ? scopes.get(elementId) : undefined),
|
|
49
|
+
() => undefined
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useViewScopeStoreVersion() {
|
|
54
|
+
return useSyncExternalStore(subscribe, () => version, () => 0);
|
|
55
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export type PanelChartConfig = {
|
|
2
|
+
title?: string;
|
|
3
|
+
color?: string;
|
|
4
|
+
colorMode?: "solid" | "gradient";
|
|
5
|
+
gradientFrom?: string;
|
|
6
|
+
gradientTo?: string;
|
|
7
|
+
gradientDirection?: "to-right" | "to-bottom" | "to-bottom-right" | "to-top-right";
|
|
8
|
+
renderMode?: "canvas" | "svg";
|
|
9
|
+
xAxisName?: string;
|
|
10
|
+
yAxisName?: string;
|
|
11
|
+
xAxisTickShow?: boolean;
|
|
12
|
+
yAxisTickShow?: boolean;
|
|
13
|
+
xAxisTickColor?: string;
|
|
14
|
+
yAxisTickColor?: string;
|
|
15
|
+
xAxisSplitLineShow?: boolean;
|
|
16
|
+
yAxisSplitLineShow?: boolean;
|
|
17
|
+
xAxisSplitLineColor?: string;
|
|
18
|
+
yAxisSplitLineColor?: string;
|
|
19
|
+
xAxisLabelColor?: string;
|
|
20
|
+
yAxisLabelColor?: string;
|
|
21
|
+
xAxisLabelFontSize?: number;
|
|
22
|
+
yAxisLabelFontSize?: number;
|
|
23
|
+
xAxisLabelAutoEllipsis?: boolean;
|
|
24
|
+
yAxisLabelAutoEllipsis?: boolean;
|
|
25
|
+
tooltipShow?: boolean;
|
|
26
|
+
tooltipTrigger?: "axis" | "item";
|
|
27
|
+
tooltipBackgroundColor?: string;
|
|
28
|
+
tooltipTextColor?: string;
|
|
29
|
+
tooltipFormatter?: string;
|
|
30
|
+
labels?: string[];
|
|
31
|
+
values?: number[];
|
|
32
|
+
/** 逗号分隔的原始数值文本,支持 scope 模版;存在时优先于 values */
|
|
33
|
+
valuesText?: string;
|
|
34
|
+
smooth?: boolean;
|
|
35
|
+
barWidth?: number;
|
|
36
|
+
pieInnerRadius?: number;
|
|
37
|
+
pieOuterRadius?: number;
|
|
38
|
+
option?: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type PanelElementStyle = {
|
|
42
|
+
backgroundColor?: string;
|
|
43
|
+
backgroundImage?: string;
|
|
44
|
+
backgroundImageRemoteUrl?: string;
|
|
45
|
+
backgroundSize?: string;
|
|
46
|
+
backgroundPosition?: string;
|
|
47
|
+
borderWidth?: number;
|
|
48
|
+
borderStyle?: "none" | "solid" | "dashed" | "dotted" | "double";
|
|
49
|
+
borderColor?: string;
|
|
50
|
+
borderRadius?: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type ReferenceCopyMode = "shallow" | "deep";
|
|
54
|
+
|
|
55
|
+
export type PanelElement = {
|
|
56
|
+
id: string;
|
|
57
|
+
layerId: string;
|
|
58
|
+
zIndex?: number;
|
|
59
|
+
name?: string;
|
|
60
|
+
locked?: boolean;
|
|
61
|
+
materialType?: string;
|
|
62
|
+
refLayerId?: string;
|
|
63
|
+
refCopyMode?: ReferenceCopyMode;
|
|
64
|
+
refSnapshot?: PanelElement[];
|
|
65
|
+
chart?: PanelChartConfig;
|
|
66
|
+
textHtml?: string;
|
|
67
|
+
textAllowInput?: boolean;
|
|
68
|
+
textFontFamily?: string;
|
|
69
|
+
textFontSize?: number;
|
|
70
|
+
textFontWeight?: string;
|
|
71
|
+
textColor?: string;
|
|
72
|
+
textLineHeight?: number;
|
|
73
|
+
textAlign?: "left" | "center" | "right" | "justify";
|
|
74
|
+
gridRows?: number;
|
|
75
|
+
gridCols?: number;
|
|
76
|
+
gridGap?: number;
|
|
77
|
+
gridPadding?: number;
|
|
78
|
+
gridSnapThreshold?: number;
|
|
79
|
+
parentGridId?: string;
|
|
80
|
+
gridSlotIndex?: number;
|
|
81
|
+
gridColSpan?: number;
|
|
82
|
+
gridRowSpan?: number;
|
|
83
|
+
mappingSourceNodeId?: string;
|
|
84
|
+
mappingSourceLayerId?: string;
|
|
85
|
+
audioSrc?: string;
|
|
86
|
+
audioRemoteUrl?: string;
|
|
87
|
+
audioPosterImage?: string;
|
|
88
|
+
audioIconPreset?: "speaker" | "music" | "headphone" | "wave";
|
|
89
|
+
audioVisualEffect?: "none" | "pulse" | "ripple";
|
|
90
|
+
audioVisualSpeed?: "slow" | "normal" | "fast";
|
|
91
|
+
mediaAutoPauseOnEdit?: boolean;
|
|
92
|
+
videoSrc?: string;
|
|
93
|
+
videoRemoteUrl?: string;
|
|
94
|
+
geometryShape?: "rect" | "circle" | "triangle" | "diamond" | "hexagon" | "star" | "heart";
|
|
95
|
+
geometryColor?: string;
|
|
96
|
+
geometryScript?: string;
|
|
97
|
+
geometrySketchDataUrl?: string;
|
|
98
|
+
style?: PanelElementStyle;
|
|
99
|
+
x: number;
|
|
100
|
+
y: number;
|
|
101
|
+
width: number;
|
|
102
|
+
height: number;
|
|
103
|
+
rotate?: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type PanelLayer = {
|
|
107
|
+
id: string;
|
|
108
|
+
name: string;
|
|
109
|
+
locked: boolean;
|
|
110
|
+
editable: boolean;
|
|
111
|
+
isPrimary?: boolean;
|
|
112
|
+
isMapping?: boolean;
|
|
113
|
+
mappingBaseLayerId?: string;
|
|
114
|
+
mergeSelected?: boolean;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type PanelHistoryItem = {
|
|
118
|
+
index: number;
|
|
119
|
+
timestamp: number;
|
|
120
|
+
label: string;
|
|
121
|
+
active: boolean;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type PanelActionResult = { ok: true } | { ok: false; reason: string };
|
|
125
|
+
|