@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,130 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @example
|
|
5
|
+
*
|
|
6
|
+
|
|
7
|
+
// 使用示例(符合你的期望调用方式)
|
|
8
|
+
const [headerName, setHeaderName] = createSignal<string | number>(123); // 支持混合类型
|
|
9
|
+
|
|
10
|
+
console.log(headerName()); // 输出: 123,类型: string | number
|
|
11
|
+
|
|
12
|
+
// 手动订阅(可选)
|
|
13
|
+
const unsubManual = headerName.subscribe((value: string | number) => {
|
|
14
|
+
console.log("手动订阅:", value);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// effect:自动追踪 headerName,当变化时打印
|
|
18
|
+
const disposeEffect = createEffect(() => {
|
|
19
|
+
console.log("effect: headerName =", headerName()); // 初始输出: effect: headerName = 123
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
setHeaderName("新标题"); //
|
|
23
|
+
// 输出:
|
|
24
|
+
// 手动订阅: 新标题
|
|
25
|
+
// effect: headerName = 新标题
|
|
26
|
+
|
|
27
|
+
setHeaderName(456); //
|
|
28
|
+
// 输出:
|
|
29
|
+
// 手动订阅: 456
|
|
30
|
+
// effect: headerName = 456
|
|
31
|
+
|
|
32
|
+
*/
|
|
33
|
+
// 全局追踪器:当前运行的 effect
|
|
34
|
+
|
|
35
|
+
// 类型定义
|
|
36
|
+
type SignalGetter<T> = (() => T) & {
|
|
37
|
+
subscribe: (cb: (value: T) => void) => () => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type SignalSetter<T> = (newValue: T) => void;
|
|
41
|
+
|
|
42
|
+
type Signal<T> = {
|
|
43
|
+
subject: BehaviorSubject<T>;
|
|
44
|
+
subscribers: Set<() => void>;
|
|
45
|
+
get: SignalGetter<T>;
|
|
46
|
+
set: SignalSetter<T>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// 全局追踪器:当前运行的 effect
|
|
50
|
+
let currentEffect: (() => void) | null = null;
|
|
51
|
+
|
|
52
|
+
// createSignal 函数:返回 [getFn, setFn],支持解构
|
|
53
|
+
export function createSignal<T>(
|
|
54
|
+
initialValue: T
|
|
55
|
+
): [SignalGetter<T>, SignalSetter<T>] {
|
|
56
|
+
const subject = new BehaviorSubject<T>(initialValue);
|
|
57
|
+
const subscribers = new Set<() => void>(); // 存储 effect 的 run 函数
|
|
58
|
+
|
|
59
|
+
// signal 对象:内部管理
|
|
60
|
+
const signal: Signal<T> = {
|
|
61
|
+
subject,
|
|
62
|
+
subscribers,
|
|
63
|
+
get: null as any, // 稍后赋值
|
|
64
|
+
set: null as any, // 稍后赋值
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// getter 函数:调用时读取值,并在 effect 中收集依赖
|
|
68
|
+
const getFn: SignalGetter<T> = function () {
|
|
69
|
+
if (currentEffect) {
|
|
70
|
+
// 收集依赖:将当前 effect 的 run 添加到信号的 subscribers
|
|
71
|
+
signal.subscribers.add(currentEffect);
|
|
72
|
+
}
|
|
73
|
+
return subject.value;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// 附加 subscribe 方法(手动订阅)
|
|
77
|
+
getFn.subscribe = (cb: (value: T) => void) => {
|
|
78
|
+
cb(subject.value); // 立即触发当前值
|
|
79
|
+
const subscription = subject.subscribe(cb);
|
|
80
|
+
return () => subscription.unsubscribe();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
signal.get = getFn;
|
|
84
|
+
|
|
85
|
+
// setter 函数:更新值,触发手动订阅者和 effect
|
|
86
|
+
const setFn: SignalSetter<T> = function (newValue: T) {
|
|
87
|
+
subject.next(newValue);
|
|
88
|
+
// 触发所有 effect subscribers
|
|
89
|
+
subscribers.forEach((run) => run());
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
signal.set = setFn;
|
|
93
|
+
|
|
94
|
+
// 返回 [getFn, setFn]
|
|
95
|
+
return [getFn, setFn];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// createEffect 函数:自动追踪依赖,并在变化时重新运行 fn
|
|
99
|
+
export function createEffect(fn: () => void): () => void {
|
|
100
|
+
let disposed = false;
|
|
101
|
+
const signalDeps = new Set<Signal<any>>(); // 存储访问过的信号对象
|
|
102
|
+
|
|
103
|
+
const effect = {
|
|
104
|
+
run: () => {
|
|
105
|
+
if (disposed) return;
|
|
106
|
+
|
|
107
|
+
// 清除旧依赖
|
|
108
|
+
signalDeps.forEach((sig) => sig.subscribers.delete(effect.run));
|
|
109
|
+
signalDeps.clear();
|
|
110
|
+
|
|
111
|
+
// 运行 fn,收集新依赖
|
|
112
|
+
currentEffect = effect.run;
|
|
113
|
+
fn();
|
|
114
|
+
currentEffect = null;
|
|
115
|
+
|
|
116
|
+
// 重新订阅新依赖
|
|
117
|
+
signalDeps.forEach((sig) => sig.subscribers.add(effect.run));
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// 初始运行
|
|
122
|
+
effect.run();
|
|
123
|
+
|
|
124
|
+
// 返回 dispose 函数
|
|
125
|
+
return () => {
|
|
126
|
+
disposed = true;
|
|
127
|
+
signalDeps.forEach((sig) => sig.subscribers.delete(effect.run));
|
|
128
|
+
signalDeps.clear();
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { JSX, useRef } from "react";
|
|
2
|
+
import { instructions } from "./instructions";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 事件需要补齐
|
|
6
|
+
* @param props
|
|
7
|
+
* @param children
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export const Template = (
|
|
11
|
+
props: Record<string, unknown>,
|
|
12
|
+
children: JSX.Element
|
|
13
|
+
) => {
|
|
14
|
+
const onHandleClick: React.MouseEventHandler<HTMLDivElement> = (e) => {
|
|
15
|
+
if (props?.id) {
|
|
16
|
+
instructions.execute(props.id + "click", { e });
|
|
17
|
+
} else {
|
|
18
|
+
if (import.meta.env.DEV) {
|
|
19
|
+
console.warn("Template 缺少 id,无法触发事件");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return <div onClick={onHandleClick}>{children}</div>;
|
|
25
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
BlueprintGraph,
|
|
4
|
+
documentToRunnableGraph,
|
|
5
|
+
getBlueprintLibraryRecord,
|
|
6
|
+
listBlueprintLibrary,
|
|
7
|
+
useBlueprintPageLifecycle,
|
|
8
|
+
type BlueprintLibraryListItem,
|
|
9
|
+
} from "@arronqzy/react-blueprint";
|
|
10
|
+
import type { LibraryBlueprintResolver, PageLifecyclePhase } from "@arronqzy/blueprint-dsl";
|
|
11
|
+
import type { State } from "../../../rx-store/src/types";
|
|
12
|
+
import { ElementsLayer } from "./components/ElementsLayer";
|
|
13
|
+
import type { PanelElement } from "./types";
|
|
14
|
+
import {
|
|
15
|
+
clearViewElementScopes,
|
|
16
|
+
getViewElementScope,
|
|
17
|
+
setViewElementScopes,
|
|
18
|
+
useViewScopeStoreVersion,
|
|
19
|
+
} from "./scope/view-scope-store";
|
|
20
|
+
import { resolvePanelElementScope } from "./utils/scope-template";
|
|
21
|
+
import {
|
|
22
|
+
getWorkspaceProject,
|
|
23
|
+
type WorkspaceProjectRecord,
|
|
24
|
+
} from "./library/workspace-project-db";
|
|
25
|
+
import { readWorkspacePreviewCache } from "./library/workspace-project-cache";
|
|
26
|
+
import { subscribeWorkspaceProjectUpdates } from "./library/workspace-project-sync";
|
|
27
|
+
import {
|
|
28
|
+
computePanelSceneBounds,
|
|
29
|
+
getActiveLayerId,
|
|
30
|
+
normalizeImportedPanelState,
|
|
31
|
+
notifyPreviewLayoutChanged,
|
|
32
|
+
parseAllPanelElements,
|
|
33
|
+
parsePanelLayers,
|
|
34
|
+
resolvePreviewLayerElements,
|
|
35
|
+
} from "./utils/panelStateIO";
|
|
36
|
+
|
|
37
|
+
const PREVIEW_BOOT_PHASES: PageLifecyclePhase[] = ["mounted"];
|
|
38
|
+
|
|
39
|
+
export type ReactViewOnlinePreviewProps = {
|
|
40
|
+
projectId: string;
|
|
41
|
+
previewInstanceId?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function applyTitleIcon(titleIconDataUrl?: string) {
|
|
45
|
+
if (!titleIconDataUrl) return;
|
|
46
|
+
for (const rel of ["icon", "shortcut icon"]) {
|
|
47
|
+
document.querySelector(`link[rel='${rel}']`)?.remove();
|
|
48
|
+
const link = document.createElement("link");
|
|
49
|
+
link.rel = rel;
|
|
50
|
+
link.type = "image/png";
|
|
51
|
+
link.href = titleIconDataUrl;
|
|
52
|
+
document.head.appendChild(link);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function loadWorkspaceRecord(projectId: string): Promise<WorkspaceProjectRecord | null> {
|
|
57
|
+
const fromDb = await getWorkspaceProject(projectId);
|
|
58
|
+
if (fromDb) return fromDb;
|
|
59
|
+
return readWorkspacePreviewCache(projectId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function ReactViewOnlinePreview({
|
|
63
|
+
projectId,
|
|
64
|
+
previewInstanceId,
|
|
65
|
+
}: ReactViewOnlinePreviewProps) {
|
|
66
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
67
|
+
const [panelState, setPanelState] = useState<State | null>(null);
|
|
68
|
+
const [projectRevision, setProjectRevision] = useState(0);
|
|
69
|
+
const [blueprintGraph, setBlueprintGraph] = useState(() => BlueprintGraph.empty());
|
|
70
|
+
const [blueprintLibraryItems, setBlueprintLibraryItems] = useState<
|
|
71
|
+
BlueprintLibraryListItem[]
|
|
72
|
+
>([]);
|
|
73
|
+
const [layoutRevision, setLayoutRevision] = useState(0);
|
|
74
|
+
const [layoutReady, setLayoutReady] = useState(false);
|
|
75
|
+
|
|
76
|
+
const sceneRef = useRef<HTMLDivElement | null>(null);
|
|
77
|
+
|
|
78
|
+
const layers = useMemo(
|
|
79
|
+
() => (panelState ? parsePanelLayers(panelState) : []),
|
|
80
|
+
[panelState]
|
|
81
|
+
);
|
|
82
|
+
const activeLayerId = useMemo(
|
|
83
|
+
() => (panelState ? getActiveLayerId(panelState) : "layer-1"),
|
|
84
|
+
[panelState]
|
|
85
|
+
);
|
|
86
|
+
const allElements = useMemo(
|
|
87
|
+
() => (panelState ? parseAllPanelElements(panelState) : []),
|
|
88
|
+
[panelState]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const loadProject = useCallback(async () => {
|
|
92
|
+
const record = await loadWorkspaceRecord(projectId);
|
|
93
|
+
if (!record) {
|
|
94
|
+
setLoadError("工作区不存在或已被删除");
|
|
95
|
+
setPanelState(null);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const normalized = normalizeImportedPanelState(record.panelState);
|
|
100
|
+
if (!normalized) {
|
|
101
|
+
setLoadError("工作区数据格式无效");
|
|
102
|
+
setPanelState(null);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setLoadError(null);
|
|
107
|
+
clearViewElementScopes();
|
|
108
|
+
setPanelState(normalized);
|
|
109
|
+
setBlueprintGraph(BlueprintGraph.fromDocument(record.blueprintDocument));
|
|
110
|
+
document.title = record.productName.trim() || record.name || "预览";
|
|
111
|
+
applyTitleIcon(record.titleIconDataUrl);
|
|
112
|
+
setProjectRevision((v) => v + 1);
|
|
113
|
+
}, [projectId]);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
void loadProject();
|
|
117
|
+
}, [loadProject]);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
return subscribeWorkspaceProjectUpdates(projectId, () => {
|
|
121
|
+
void loadProject();
|
|
122
|
+
});
|
|
123
|
+
}, [loadProject, projectId]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
let cancelled = false;
|
|
127
|
+
void listBlueprintLibrary().then((items) => {
|
|
128
|
+
if (!cancelled) setBlueprintLibraryItems(items);
|
|
129
|
+
});
|
|
130
|
+
return () => {
|
|
131
|
+
cancelled = true;
|
|
132
|
+
};
|
|
133
|
+
}, [projectRevision]);
|
|
134
|
+
|
|
135
|
+
const blueprintLibraryNameById = useMemo(
|
|
136
|
+
() => new Map(blueprintLibraryItems.map((item) => [item.id, item.name])),
|
|
137
|
+
[blueprintLibraryItems]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const resolveLibraryBlueprint = useCallback<LibraryBlueprintResolver>(
|
|
141
|
+
async (libraryBlueprintId) => {
|
|
142
|
+
const record = await getBlueprintLibraryRecord(libraryBlueprintId);
|
|
143
|
+
if (!record) return null;
|
|
144
|
+
const items = await listBlueprintLibrary();
|
|
145
|
+
const nameById = new Map(items.map((item) => [item.id, item.name]));
|
|
146
|
+
return documentToRunnableGraph(record.document, { libraryNameById: nameById });
|
|
147
|
+
},
|
|
148
|
+
[]
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const handleViewScopeUpdate = useCallback(
|
|
152
|
+
(viewElementIds: string[], scope: unknown) => {
|
|
153
|
+
setViewElementScopes(viewElementIds, scope);
|
|
154
|
+
},
|
|
155
|
+
[]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const layerElements = useMemo(
|
|
159
|
+
() => resolvePreviewLayerElements(allElements, layers, activeLayerId),
|
|
160
|
+
[activeLayerId, allElements, layers]
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const scopeStoreVersion = useViewScopeStoreVersion();
|
|
164
|
+
const scopedLayerElements = useMemo(() => {
|
|
165
|
+
void scopeStoreVersion;
|
|
166
|
+
return layerElements.map((el) =>
|
|
167
|
+
resolvePanelElementScope(el, getViewElementScope(el.id))
|
|
168
|
+
);
|
|
169
|
+
}, [layerElements, scopeStoreVersion]);
|
|
170
|
+
|
|
171
|
+
const sceneBounds = useMemo(
|
|
172
|
+
() => computePanelSceneBounds(scopedLayerElements),
|
|
173
|
+
[scopedLayerElements]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const displayElements = useMemo(
|
|
177
|
+
() =>
|
|
178
|
+
scopedLayerElements.map((el) => ({
|
|
179
|
+
...el,
|
|
180
|
+
x: el.x - sceneBounds.minX,
|
|
181
|
+
y: el.y - sceneBounds.minY,
|
|
182
|
+
})),
|
|
183
|
+
[sceneBounds.minX, sceneBounds.minY, scopedLayerElements]
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const applySceneFit = useCallback(() => {
|
|
187
|
+
const scene = sceneRef.current;
|
|
188
|
+
if (!scene) return;
|
|
189
|
+
const vw = window.innerWidth || 1;
|
|
190
|
+
const vh = window.innerHeight || 1;
|
|
191
|
+
const sw = Math.max(1, sceneBounds.width);
|
|
192
|
+
const sh = Math.max(1, sceneBounds.height);
|
|
193
|
+
const scaleX = vw / sw;
|
|
194
|
+
const scaleY = vh / sh;
|
|
195
|
+
if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY)) return;
|
|
196
|
+
scene.style.transform = `scale(${scaleX}, ${scaleY})`;
|
|
197
|
+
setLayoutRevision((v) => v + 1);
|
|
198
|
+
notifyPreviewLayoutChanged();
|
|
199
|
+
}, [sceneBounds.height, sceneBounds.width]);
|
|
200
|
+
|
|
201
|
+
useLayoutEffect(() => {
|
|
202
|
+
if (!panelState || layerElements.length === 0) return;
|
|
203
|
+
|
|
204
|
+
setLayoutReady(false);
|
|
205
|
+
applySceneFit();
|
|
206
|
+
let raf2 = 0;
|
|
207
|
+
const raf1 = requestAnimationFrame(() => {
|
|
208
|
+
applySceneFit();
|
|
209
|
+
raf2 = requestAnimationFrame(() => {
|
|
210
|
+
notifyPreviewLayoutChanged();
|
|
211
|
+
setLayoutReady(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
return () => {
|
|
215
|
+
cancelAnimationFrame(raf1);
|
|
216
|
+
if (raf2) cancelAnimationFrame(raf2);
|
|
217
|
+
};
|
|
218
|
+
}, [
|
|
219
|
+
applySceneFit,
|
|
220
|
+
displayElements.length,
|
|
221
|
+
layerElements.length,
|
|
222
|
+
panelState,
|
|
223
|
+
projectRevision,
|
|
224
|
+
sceneBounds.height,
|
|
225
|
+
sceneBounds.width,
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
const lifecycleReady =
|
|
229
|
+
!!panelState && layerElements.length > 0 && layoutReady;
|
|
230
|
+
|
|
231
|
+
useBlueprintPageLifecycle({
|
|
232
|
+
graph: blueprintGraph,
|
|
233
|
+
active: true,
|
|
234
|
+
enabled: lifecycleReady,
|
|
235
|
+
bootPhases: PREVIEW_BOOT_PHASES,
|
|
236
|
+
bootKey: projectRevision,
|
|
237
|
+
waitForPageReady: true,
|
|
238
|
+
onUpdated: `${activeLayerId}|${layerElements.length}|${projectRevision}|${layoutRevision}`,
|
|
239
|
+
resolveLibraryBlueprint,
|
|
240
|
+
libraryNameById: blueprintLibraryNameById,
|
|
241
|
+
rootLibraryBlueprintId: null,
|
|
242
|
+
onViewScopeUpdate: handleViewScopeUpdate,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
window.addEventListener("resize", applySceneFit);
|
|
247
|
+
return () => window.removeEventListener("resize", applySceneFit);
|
|
248
|
+
}, [applySceneFit]);
|
|
249
|
+
|
|
250
|
+
const noopUpdate = useCallback((_id: string, _patch: Partial<PanelElement>) => {}, []);
|
|
251
|
+
const noopSelect = useCallback((_ids: string[]) => {}, []);
|
|
252
|
+
|
|
253
|
+
if (loadError) {
|
|
254
|
+
return (
|
|
255
|
+
<div className="flex min-h-[100vh] w-full items-center justify-center bg-white px-6 text-center text-sm text-gray-600">
|
|
256
|
+
{loadError}
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!panelState) {
|
|
262
|
+
return (
|
|
263
|
+
<div className="flex min-h-[100vh] w-full items-center justify-center bg-white text-sm text-gray-600">
|
|
264
|
+
加载预览中…
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (layerElements.length === 0) {
|
|
270
|
+
return (
|
|
271
|
+
<div className="flex min-h-[100vh] w-full flex-col items-center justify-center gap-2 bg-white px-6 text-center text-sm text-gray-600">
|
|
272
|
+
<div>当前工作区没有可预览的视图节点</div>
|
|
273
|
+
<div className="text-xs text-gray-400">
|
|
274
|
+
请先在编辑器中点击「同步」或「创建工作区」后再预览
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
className="min-h-[100vh] w-full overflow-hidden bg-white text-gray-900"
|
|
283
|
+
data-preview-mode="online"
|
|
284
|
+
data-project-id={projectId}
|
|
285
|
+
data-preview-instance-id={previewInstanceId ?? ""}
|
|
286
|
+
data-preview-node-count={String(displayElements.length)}
|
|
287
|
+
>
|
|
288
|
+
<div
|
|
289
|
+
id="preview-root"
|
|
290
|
+
className="overflow-hidden"
|
|
291
|
+
style={{ width: "100vw", height: "100vh" }}
|
|
292
|
+
>
|
|
293
|
+
<div
|
|
294
|
+
ref={sceneRef}
|
|
295
|
+
id="preview-scene"
|
|
296
|
+
className="relative shrink-0 origin-top-left"
|
|
297
|
+
style={{
|
|
298
|
+
width: sceneBounds.width,
|
|
299
|
+
height: sceneBounds.height,
|
|
300
|
+
transformOrigin: "left top",
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
<ElementsLayer
|
|
304
|
+
elements={displayElements}
|
|
305
|
+
allElements={allElements}
|
|
306
|
+
selectedIds={[]}
|
|
307
|
+
onSelectIds={noopSelect}
|
|
308
|
+
updateElement={noopUpdate}
|
|
309
|
+
layerLocked
|
|
310
|
+
previewMode
|
|
311
|
+
previewLayoutKey={layoutRevision}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function parseOnlinePreviewSearchParams(search: string): {
|
|
320
|
+
projectId: string;
|
|
321
|
+
previewInstanceId?: string;
|
|
322
|
+
} | null {
|
|
323
|
+
const params = new URLSearchParams(search);
|
|
324
|
+
if (params.get("preview") !== "online") return null;
|
|
325
|
+
const projectId = params.get("projectId") ?? params.get("id");
|
|
326
|
+
if (!projectId) return null;
|
|
327
|
+
return {
|
|
328
|
+
projectId,
|
|
329
|
+
previewInstanceId: params.get("pid") ?? undefined,
|
|
330
|
+
};
|
|
331
|
+
}
|