@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.
Files changed (112) hide show
  1. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
  2. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
  3. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
  4. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
  5. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
  6. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
  7. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
  8. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
  9. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
  10. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
  11. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
  12. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
  13. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
  14. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
  15. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
  16. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
  17. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
  18. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
  19. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
  20. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
  21. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
  22. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
  23. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
  24. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
  25. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
  26. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
  27. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
  28. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
  29. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
  30. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
  31. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
  32. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
  33. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
  34. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
  35. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
  36. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
  37. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
  38. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
  39. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
  40. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
  41. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
  42. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
  43. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
  44. package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
  45. package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
  46. package/.pnpm-cache/metadata/registry.npmjs.org/pnpm.json +1 -0
  47. package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
  48. package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
  49. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
  50. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
  51. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
  52. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
  53. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
  54. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
  55. package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
  56. package/.turbo/turbo-lint.log +4 -0
  57. package/dist/styles.css +2624 -0
  58. package/env.d.ts +11 -0
  59. package/operation-guide.md +275 -0
  60. package/package.json +43 -0
  61. package/postcss.config.cjs +7 -0
  62. package/readme.md +21 -0
  63. package/src/core/createAction.ts +5 -0
  64. package/src/core/createView.ts +28 -0
  65. package/src/core/instructions.tsx +57 -0
  66. package/src/core/nodePool.ts +127 -0
  67. package/src/core/signal.ts +130 -0
  68. package/src/core/template.tsx +25 -0
  69. package/src/index.ts +7 -0
  70. package/src/panel/ReactViewOnlinePreview.tsx +331 -0
  71. package/src/panel/ReactViewPanel.tsx +3221 -0
  72. package/src/panel/components/ElementsLayer.tsx +891 -0
  73. package/src/panel/components/MaterialSidebar.tsx +1142 -0
  74. package/src/panel/components/MoveableLayer.tsx +803 -0
  75. package/src/panel/components/PanelCanvas.tsx +660 -0
  76. package/src/panel/components/PanelConfigSidebar.tsx +3703 -0
  77. package/src/panel/components/PanelRulers.tsx +89 -0
  78. package/src/panel/components/SelectLayer.tsx +106 -0
  79. package/src/panel/components/ViewElementScopePanel.tsx +79 -0
  80. package/src/panel/components/WorkspaceConfigSidebar.tsx +158 -0
  81. package/src/panel/components/WorkspaceProjectNav.tsx +220 -0
  82. package/src/panel/components/WorkspaceStageSplit.tsx +179 -0
  83. package/src/panel/components/scope-config/ScopeConfigContext.tsx +170 -0
  84. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +250 -0
  85. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +59 -0
  86. package/src/panel/constants/messages.ts +34 -0
  87. package/src/panel/constants/zIndex.ts +6 -0
  88. package/src/panel/hooks/usePanelElements.ts +1108 -0
  89. package/src/panel/hooks/useWorkspaceProjects.ts +278 -0
  90. package/src/panel/index.tsx +3 -0
  91. package/src/panel/library/workspace-project-cache.ts +23 -0
  92. package/src/panel/library/workspace-project-db.ts +112 -0
  93. package/src/panel/library/workspace-project-sync.ts +41 -0
  94. package/src/panel/library/workspace-snapshot.ts +27 -0
  95. package/src/panel/scope/view-scope-store.ts +55 -0
  96. package/src/panel/types.ts +125 -0
  97. package/src/panel/utils/chartOptionBuilder.ts +316 -0
  98. package/src/panel/utils/gridPlacement.ts +189 -0
  99. package/src/panel/utils/mappingLayerOps.ts +142 -0
  100. package/src/panel/utils/panelElementDefaults.ts +161 -0
  101. package/src/panel/utils/panelElementNodes.ts +35 -0
  102. package/src/panel/utils/panelStateIO.ts +124 -0
  103. package/src/panel/utils/scope-autocomplete.ts +114 -0
  104. package/src/panel/utils/scope-field-labels.ts +45 -0
  105. package/src/panel/utils/scope-template-warnings.ts +135 -0
  106. package/src/panel/utils/scope-template.ts +75 -0
  107. package/src/panel/utils/updateElementDraft.ts +221 -0
  108. package/src/panel/viewportZoom.ts +26 -0
  109. package/src/tailwind.css +58 -0
  110. package/src/types/react-infinite-viewer.d.ts +39 -0
  111. package/tailwind.config.ts +7 -0
  112. package/tsconfig.json +14 -0
@@ -0,0 +1,161 @@
1
+ import type { PanelChartConfig, PanelLayer } from "../types";
2
+
3
+ const DEFAULT_NODE_NAME_MAP: Record<string, string> = {
4
+ bar: "柱状图",
5
+ line: "折线图",
6
+ pie: "饼图",
7
+ area: "面积图",
8
+ scatter: "散点图",
9
+ radar: "雷达图",
10
+ gauge: "仪表盘",
11
+ funnel: "漏斗图",
12
+ text: "文本",
13
+ grid: "网格布局",
14
+ image: "图片",
15
+ video: "视频",
16
+ audio: "音频",
17
+ reference: "引用组件",
18
+ geometry: "几何",
19
+ };
20
+
21
+ export const DEFAULT_LAYER_ID = "layer-1";
22
+
23
+ export function getDefaultNodeName(materialType: string): string {
24
+ return DEFAULT_NODE_NAME_MAP[materialType] ?? materialType;
25
+ }
26
+
27
+ export const DEFAULT_LAYER: PanelLayer = {
28
+ id: DEFAULT_LAYER_ID,
29
+ name: "图层1",
30
+ locked: false,
31
+ editable: false,
32
+ isPrimary: true,
33
+ isMapping: false,
34
+ mappingBaseLayerId: undefined,
35
+ };
36
+
37
+ export function normalizePrimaryLayer(layers: PanelLayer[]): PanelLayer[] {
38
+ if (layers.length === 0) return [DEFAULT_LAYER];
39
+ const explicitPrimary = layers.find((layer) => layer.isPrimary);
40
+ const primaryId = explicitPrimary?.id ?? layers[0].id;
41
+ return layers.map((layer) => {
42
+ if (layer.id === primaryId) return { ...layer, isPrimary: true };
43
+ return { ...layer, isPrimary: false };
44
+ });
45
+ }
46
+
47
+ export function randomId(prefix: string) {
48
+ return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
49
+ }
50
+
51
+ export function getDefaultSizeByMaterial(materialType: string) {
52
+ switch (materialType) {
53
+ case "text":
54
+ return { width: 180, height: 56 };
55
+ case "rect":
56
+ return { width: 180, height: 120 };
57
+ case "grid":
58
+ return { width: 320, height: 220 };
59
+ case "image":
60
+ return { width: 220, height: 140 };
61
+ case "video":
62
+ return { width: 260, height: 150 };
63
+ case "audio":
64
+ return { width: 260, height: 90 };
65
+ case "gauge":
66
+ return { width: 260, height: 180 };
67
+ case "reference":
68
+ return { width: 280, height: 180 };
69
+ case "geometry":
70
+ return { width: 220, height: 220 };
71
+ default:
72
+ return { width: 220, height: 130 };
73
+ }
74
+ }
75
+
76
+ export function getDefaultTextContent(materialType: string) {
77
+ if (materialType !== "text") return {};
78
+ return {
79
+ textHtml: "<p>双击输入文本</p>",
80
+ textAllowInput: true,
81
+ textFontSize: 14,
82
+ textFontWeight: "400",
83
+ textLineHeight: 1.6,
84
+ textAlign: "left",
85
+ } as const;
86
+ }
87
+
88
+ export function getDefaultGridConfig(materialType: string) {
89
+ if (materialType !== "grid") return {};
90
+ return {
91
+ gridRows: 2,
92
+ gridCols: 3,
93
+ gridGap: 8,
94
+ gridPadding: 10,
95
+ gridSnapThreshold: 36,
96
+ } as const;
97
+ }
98
+
99
+ export function getDefaultChartConfig(materialType: string): PanelChartConfig | undefined {
100
+ if (!["bar", "line", "pie", "area", "scatter", "radar", "gauge", "funnel"].includes(materialType))
101
+ return undefined;
102
+ const common = {
103
+ color: "#3b82f6",
104
+ renderMode: "canvas" as const,
105
+ labels: ["A", "B", "C", "D"],
106
+ values: [12, 18, 9, 24],
107
+ };
108
+ if (materialType === "bar") {
109
+ return {
110
+ title: "柱状图",
111
+ ...common,
112
+ barWidth: 24,
113
+ };
114
+ }
115
+ if (materialType === "line") {
116
+ return {
117
+ title: "折线图",
118
+ ...common,
119
+ smooth: true,
120
+ };
121
+ }
122
+ if (materialType === "area") {
123
+ return {
124
+ title: "面积图",
125
+ ...common,
126
+ smooth: true,
127
+ };
128
+ }
129
+ if (materialType === "scatter") {
130
+ return {
131
+ title: "散点图",
132
+ ...common,
133
+ };
134
+ }
135
+ if (materialType === "radar") {
136
+ return {
137
+ title: "雷达图",
138
+ ...common,
139
+ };
140
+ }
141
+ if (materialType === "gauge") {
142
+ return {
143
+ title: "仪表盘",
144
+ color: "#3b82f6",
145
+ renderMode: "canvas",
146
+ values: [68],
147
+ };
148
+ }
149
+ if (materialType === "funnel") {
150
+ return {
151
+ title: "漏斗图",
152
+ ...common,
153
+ };
154
+ }
155
+ return {
156
+ title: "饼图",
157
+ ...common,
158
+ pieInnerRadius: 30,
159
+ pieOuterRadius: 65,
160
+ };
161
+ }
@@ -0,0 +1,35 @@
1
+ import type { Node } from "../../../../rx-store/src/types";
2
+ import type { PanelElement } from "../types";
3
+
4
+ export function isPanelElementNode(node: Node): boolean {
5
+ const props = node.props as Partial<PanelElement> | undefined;
6
+ return !!props && typeof props.id === "string" && typeof props.layerId === "string";
7
+ }
8
+
9
+ export function clonePanelElement(el: PanelElement): PanelElement {
10
+ return {
11
+ ...el,
12
+ chart: el.chart ? { ...el.chart, option: el.chart.option ? { ...el.chart.option } : undefined } : undefined,
13
+ style: el.style ? { ...el.style } : undefined,
14
+ refSnapshot: el.refSnapshot ? el.refSnapshot.map(clonePanelElement) : undefined,
15
+ };
16
+ }
17
+
18
+ export function buildDeepReferenceSnapshot(
19
+ allElements: PanelElement[],
20
+ layerId: string,
21
+ visitedLayers = new Set<string>()
22
+ ): PanelElement[] {
23
+ if (visitedLayers.has(layerId)) return [];
24
+ const nextVisited = new Set(visitedLayers);
25
+ nextVisited.add(layerId);
26
+ const layerNodes = allElements.filter((el) => el.layerId === layerId);
27
+ return layerNodes.map((el) => {
28
+ const cloned = clonePanelElement(el);
29
+ if (cloned.materialType === "reference" && cloned.refLayerId) {
30
+ cloned.refCopyMode = "deep";
31
+ cloned.refSnapshot = buildDeepReferenceSnapshot(allElements, cloned.refLayerId, nextVisited);
32
+ }
33
+ return cloned;
34
+ });
35
+ }
@@ -0,0 +1,124 @@
1
+ import type { State } from "../../../../rx-store/src/types";
2
+ import type { PanelElement, PanelLayer } from "../types";
3
+ import {
4
+ DEFAULT_LAYER,
5
+ DEFAULT_LAYER_ID,
6
+ normalizePrimaryLayer,
7
+ } from "./panelElementDefaults";
8
+
9
+ export function normalizeImportedPanelState(state: unknown): State | null {
10
+ if (!state || typeof state !== "object") return null;
11
+ const raw = state as State;
12
+ if (!raw.root || typeof raw.root !== "object") return null;
13
+ if (!raw.root.id || !raw.root.type) return null;
14
+
15
+ const next = JSON.parse(JSON.stringify(raw)) as State;
16
+ next.root.children = Array.isArray(next.root.children) ? next.root.children : [];
17
+ next.variables = next.variables ?? {};
18
+ if (!Array.isArray(next.variables.layers)) {
19
+ next.variables.layers = [DEFAULT_LAYER];
20
+ }
21
+ next.variables.layers = normalizePrimaryLayer(next.variables.layers as PanelLayer[]);
22
+ if (typeof next.variables.activeLayerId !== "string") {
23
+ next.variables.activeLayerId =
24
+ (next.variables.layers[0] as PanelLayer | undefined)?.id ?? DEFAULT_LAYER_ID;
25
+ }
26
+ return next;
27
+ }
28
+
29
+ export function parseAllPanelElements(state: State): PanelElement[] {
30
+ return (state.root.children ?? [])
31
+ .filter((node) => {
32
+ const props = node.props as Partial<PanelElement> | undefined;
33
+ if (!props || typeof props.layerId !== "string") return false;
34
+ const id = props.id ?? node.id;
35
+ return typeof id === "string" && id.length > 0;
36
+ })
37
+ .map((node) => {
38
+ const props = node.props as PanelElement;
39
+ return {
40
+ ...props,
41
+ id: props.id ?? node.id,
42
+ zIndex: typeof props.zIndex === "number" ? props.zIndex : 1,
43
+ };
44
+ });
45
+ }
46
+
47
+ export function parsePanelLayers(state: State): PanelLayer[] {
48
+ const raw = (state.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
49
+ return normalizePrimaryLayer(raw);
50
+ }
51
+
52
+ export function getActiveLayerId(state: State): string {
53
+ return (state.variables?.activeLayerId as string | undefined) ?? DEFAULT_LAYER_ID;
54
+ }
55
+
56
+ export function resolvePreviewLayerElements(
57
+ allElements: PanelElement[],
58
+ layers: PanelLayer[],
59
+ activeLayerId: string
60
+ ): PanelElement[] {
61
+ const primary = layers.find((layer) => layer.isPrimary) ?? layers[0];
62
+ if (primary) {
63
+ const onPrimary = allElements.filter((el) => el.layerId === primary.id);
64
+ if (onPrimary.length > 0) return onPrimary;
65
+ }
66
+ const onActive = allElements.filter((el) => el.layerId === activeLayerId);
67
+ if (onActive.length > 0) return onActive;
68
+ const mappingLayerIds = new Set(
69
+ layers.filter((layer) => layer.isMapping).map((layer) => layer.id)
70
+ );
71
+ const nonMapping = allElements.filter((el) => !mappingLayerIds.has(el.layerId));
72
+ return nonMapping.length > 0 ? nonMapping : allElements;
73
+ }
74
+
75
+ export type PanelSceneBounds = {
76
+ minX: number;
77
+ minY: number;
78
+ width: number;
79
+ height: number;
80
+ };
81
+
82
+ export function computePanelSceneBounds(elements: PanelElement[]): PanelSceneBounds {
83
+ if (!elements.length) {
84
+ return { minX: 0, minY: 0, width: 1, height: 1 };
85
+ }
86
+
87
+ let minX = Infinity;
88
+ let minY = Infinity;
89
+ let maxX = -Infinity;
90
+ let maxY = -Infinity;
91
+
92
+ for (const el of elements) {
93
+ const w = Math.max(1, el.width);
94
+ const h = Math.max(1, el.height);
95
+ const rad = ((el.rotate ?? 0) * Math.PI) / 180;
96
+ const absCos = Math.abs(Math.cos(rad));
97
+ const absSin = Math.abs(Math.sin(rad));
98
+ const bw = w * absCos + h * absSin;
99
+ const bh = w * absSin + h * absCos;
100
+ const cx = el.x + w / 2;
101
+ const cy = el.y + h / 2;
102
+ const left = cx - bw / 2;
103
+ const top = cy - bh / 2;
104
+ const right = cx + bw / 2;
105
+ const bottom = cy + bh / 2;
106
+ minX = Math.min(minX, left);
107
+ minY = Math.min(minY, top);
108
+ maxX = Math.max(maxX, right);
109
+ maxY = Math.max(maxY, bottom);
110
+ }
111
+
112
+ return {
113
+ minX,
114
+ minY,
115
+ width: Math.max(1, maxX - minX),
116
+ height: Math.max(1, maxY - minY),
117
+ };
118
+ }
119
+
120
+ export const PREVIEW_LAYOUT_EVENT = "arronqzy-preview-layout";
121
+
122
+ export function notifyPreviewLayoutChanged() {
123
+ window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
124
+ }
@@ -0,0 +1,114 @@
1
+ export type ScopeAutocompleteState = {
2
+ templateStart: number;
3
+ cursor: number;
4
+ path: string[];
5
+ partialKey: string;
6
+ };
7
+
8
+ function getValueAtPath(scope: unknown, path: string[]): unknown {
9
+ let current: unknown = scope;
10
+ for (const part of path) {
11
+ if (current === null || typeof current !== "object") return undefined;
12
+ current = (current as Record<string, unknown>)[part];
13
+ }
14
+ return current;
15
+ }
16
+
17
+ function listObjectKeys(value: unknown): string[] {
18
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
19
+ return [];
20
+ }
21
+ return Object.keys(value as Record<string, unknown>);
22
+ }
23
+
24
+ /** 解析光标处未闭合的 scope 模版表达式 */
25
+ export function parseScopeAutocomplete(
26
+ text: string,
27
+ cursor: number,
28
+ scope: unknown
29
+ ): ScopeAutocompleteState | null {
30
+ const before = text.slice(0, cursor);
31
+ const templateStart = before.lastIndexOf("{");
32
+ if (templateStart < 0) return null;
33
+
34
+ const inner = before.slice(templateStart + 1);
35
+ if (inner.includes("}")) return null;
36
+ if (!inner.startsWith("scope")) return null;
37
+
38
+ let body = inner.slice("scope".length);
39
+ if (body === "" || body === "?") {
40
+ return { templateStart, cursor, path: [], partialKey: "" };
41
+ }
42
+ if (!body.startsWith("?.")) return null;
43
+
44
+ body = body.slice(2);
45
+ if (body === "") {
46
+ return { templateStart, cursor, path: [], partialKey: "" };
47
+ }
48
+
49
+ const segments = body.split("?.");
50
+ const endsWithSeparator = inner.endsWith("?.");
51
+
52
+ if (endsWithSeparator) {
53
+ const path = segments.filter((segment) => segment.length > 0);
54
+ return { templateStart, cursor, path, partialKey: "" };
55
+ }
56
+
57
+ const lastSegment = segments[segments.length - 1] ?? "";
58
+ const prefixSegments = segments.slice(0, -1).filter((segment) => segment.length > 0);
59
+ const parent = getValueAtPath(scope, prefixSegments);
60
+ const keys = listObjectKeys(parent);
61
+
62
+ if (lastSegment.length === 0) {
63
+ return { templateStart, cursor, path: prefixSegments, partialKey: "" };
64
+ }
65
+
66
+ if (keys.includes(lastSegment)) {
67
+ return {
68
+ templateStart,
69
+ cursor,
70
+ path: [...prefixSegments, lastSegment],
71
+ partialKey: "",
72
+ };
73
+ }
74
+
75
+ return {
76
+ templateStart,
77
+ cursor,
78
+ path: prefixSegments,
79
+ partialKey: lastSegment,
80
+ };
81
+ }
82
+
83
+ export function getScopeAutocompleteSuggestions(
84
+ scope: unknown,
85
+ state: ScopeAutocompleteState
86
+ ): string[] {
87
+ const parent =
88
+ state.partialKey.length > 0
89
+ ? getValueAtPath(scope, state.path)
90
+ : state.path.length > 0
91
+ ? getValueAtPath(scope, state.path)
92
+ : scope;
93
+
94
+ const keys = listObjectKeys(parent);
95
+ if (state.partialKey.length === 0) return keys;
96
+ return keys.filter((key) => key.startsWith(state.partialKey));
97
+ }
98
+
99
+ export function buildScopeExpression(path: string[], selectedKey: string): string {
100
+ return `scope?.${[...path, selectedKey].join("?.")}`;
101
+ }
102
+
103
+ export function applyScopeAutocompleteSelection(
104
+ text: string,
105
+ state: ScopeAutocompleteState,
106
+ selectedKey: string
107
+ ): { value: string; cursor: number } {
108
+ const expr = buildScopeExpression(state.path, selectedKey);
109
+ const before = text.slice(0, state.templateStart + 1);
110
+ const after = text.slice(state.cursor);
111
+ const value = `${before}${expr}${after}`;
112
+ const cursor = before.length + expr.length;
113
+ return { value, cursor };
114
+ }
@@ -0,0 +1,45 @@
1
+ const LABELS: Record<string, string> = {
2
+ name: "节点名称",
3
+ textHtml: "文本内容",
4
+ textFontFamily: "文本字体",
5
+ textFontWeight: "文本字重",
6
+ textColor: "文本颜色",
7
+ "chart.title": "图表标题",
8
+ "chart.color": "图表颜色",
9
+ "chart.gradientFrom": "渐变起始色",
10
+ "chart.gradientTo": "渐变结束色",
11
+ "chart.xAxisName": "X 轴名称",
12
+ "chart.yAxisName": "Y 轴名称",
13
+ "chart.xAxisTickColor": "X 轴刻度颜色",
14
+ "chart.yAxisTickColor": "Y 轴刻度颜色",
15
+ "chart.xAxisSplitLineColor": "X 轴分割线颜色",
16
+ "chart.yAxisSplitLineColor": "Y 轴分割线颜色",
17
+ "chart.xAxisLabelColor": "X 轴标签颜色",
18
+ "chart.yAxisLabelColor": "Y 轴标签颜色",
19
+ "chart.tooltipBackgroundColor": "Tooltip 背景色",
20
+ "chart.tooltipTextColor": "Tooltip 文字色",
21
+ "chart.tooltipFormatter": "Tooltip 格式化",
22
+ "chart.valuesText": "图表数值",
23
+ "style.backgroundColor": "背景色",
24
+ "style.backgroundImage": "背景图",
25
+ "style.backgroundImageRemoteUrl": "背景图远程地址",
26
+ "style.backgroundSize": "背景尺寸",
27
+ "style.backgroundPosition": "背景位置",
28
+ "style.borderColor": "边框颜色",
29
+ audioSrc: "音频地址",
30
+ audioRemoteUrl: "音频远程地址",
31
+ videoSrc: "视频地址",
32
+ videoRemoteUrl: "视频远程地址",
33
+ geometryColor: "几何颜色",
34
+ geometryScript: "几何脚本",
35
+ };
36
+
37
+ export function resolveScopeFieldLabel(fieldId: string): string {
38
+ if (LABELS[fieldId]) return LABELS[fieldId];
39
+ const leaf = fieldId.split(".").pop() ?? fieldId;
40
+ return LABELS[fieldId] ?? leaf;
41
+ }
42
+
43
+ export function scopeFieldDomId(fieldId: string): string {
44
+ return `scope-field-${fieldId.replace(/\./g, "--")}`;
45
+ }
@@ -0,0 +1,135 @@
1
+ import type { PanelElement } from "../types";
2
+ import { hasScopeTemplate } from "./scope-template";
3
+ import { resolveScopeFieldLabel } from "./scope-field-labels";
4
+
5
+ export type ScopeTemplateWarning = {
6
+ fieldId: string;
7
+ fieldLabel: string;
8
+ template: string;
9
+ expression: string;
10
+ missingPath: string;
11
+ message: string;
12
+ };
13
+
14
+ const SCOPE_PATH_RE = /scope(?:\?\.|\.)(\w+(?:\?\.\w+)*)/g;
15
+
16
+ const SKIP_FIELD_PREFIXES = new Set(["refSnapshot", "geometrySketchDataUrl"]);
17
+
18
+ function extractScopePropertyPaths(expression: string): string[] {
19
+ const paths = new Set<string>();
20
+ let match: RegExpExecArray | null;
21
+ const re = new RegExp(SCOPE_PATH_RE.source, "g");
22
+ while ((match = re.exec(expression)) !== null) {
23
+ paths.add(match[1].replace(/\?\./g, "."));
24
+ }
25
+ return [...paths];
26
+ }
27
+
28
+ function pathExistsInScope(scope: unknown, path: string): boolean {
29
+ if (scope === null || scope === undefined) return false;
30
+ const parts = path.split(".").filter(Boolean);
31
+ let current: unknown = scope;
32
+ for (const part of parts) {
33
+ if (current === null || typeof current !== "object" || Array.isArray(current)) {
34
+ return false;
35
+ }
36
+ if (!Object.prototype.hasOwnProperty.call(current, part)) {
37
+ return false;
38
+ }
39
+ current = (current as Record<string, unknown>)[part];
40
+ }
41
+ return true;
42
+ }
43
+
44
+ function analyzeTemplateValue(
45
+ fieldId: string,
46
+ value: string,
47
+ scope: unknown
48
+ ): ScopeTemplateWarning[] {
49
+ if (!hasScopeTemplate(value)) return [];
50
+
51
+ const warnings: ScopeTemplateWarning[] = [];
52
+ const fieldLabel = resolveScopeFieldLabel(fieldId);
53
+ const expressionRe = /\{([^}]+)\}/g;
54
+ let match: RegExpExecArray | null;
55
+
56
+ while ((match = expressionRe.exec(value)) !== null) {
57
+ const expression = match[1].trim();
58
+ const paths = extractScopePropertyPaths(expression);
59
+ for (const path of paths) {
60
+ if (pathExistsInScope(scope, path)) continue;
61
+ warnings.push({
62
+ fieldId,
63
+ fieldLabel,
64
+ template: value,
65
+ expression,
66
+ missingPath: path,
67
+ message: `「${fieldLabel}」引用了 scope.${path},但当前 scope 数据中并没有「${path}」字段,建议检查一下。`,
68
+ });
69
+ }
70
+ }
71
+
72
+ return warnings;
73
+ }
74
+
75
+ function shouldSkipField(fieldId: string): boolean {
76
+ return [...SKIP_FIELD_PREFIXES].some((prefix) => fieldId.startsWith(prefix));
77
+ }
78
+
79
+ function walkElementStrings(
80
+ value: unknown,
81
+ fieldId: string,
82
+ scope: unknown,
83
+ warnings: ScopeTemplateWarning[]
84
+ ) {
85
+ if (shouldSkipField(fieldId)) return;
86
+
87
+ if (typeof value === "string") {
88
+ warnings.push(...analyzeTemplateValue(fieldId, value, scope));
89
+ return;
90
+ }
91
+
92
+ if (Array.isArray(value)) {
93
+ value.forEach((item, index) => {
94
+ walkElementStrings(item, `${fieldId}.${index}`, scope, warnings);
95
+ });
96
+ return;
97
+ }
98
+
99
+ if (value && typeof value === "object") {
100
+ for (const [key, nested] of Object.entries(value)) {
101
+ const nextId = fieldId ? `${fieldId}.${key}` : key;
102
+ walkElementStrings(nested, nextId, scope, warnings);
103
+ }
104
+ }
105
+ }
106
+
107
+ export function collectElementScopeWarnings(
108
+ element: PanelElement,
109
+ scope: unknown
110
+ ): ScopeTemplateWarning[] {
111
+ if (scope === undefined) return [];
112
+
113
+ const warnings: ScopeTemplateWarning[] = [];
114
+ walkElementStrings(element, "", scope, warnings);
115
+
116
+ const seen = new Set<string>();
117
+ return warnings.filter((warning) => {
118
+ const key = `${warning.fieldId}|${warning.missingPath}|${warning.expression}`;
119
+ if (seen.has(key)) return false;
120
+ seen.add(key);
121
+ return true;
122
+ });
123
+ }
124
+
125
+ export function groupScopeWarningsByField(
126
+ warnings: ScopeTemplateWarning[]
127
+ ): Map<string, ScopeTemplateWarning[]> {
128
+ const map = new Map<string, ScopeTemplateWarning[]>();
129
+ for (const warning of warnings) {
130
+ const list = map.get(warning.fieldId) ?? [];
131
+ list.push(warning);
132
+ map.set(warning.fieldId, list);
133
+ }
134
+ return map;
135
+ }
@@ -0,0 +1,75 @@
1
+ import type { PanelElement } from "../types";
2
+
3
+ const SCOPE_TEMPLATE_RE = /\{[^}]+\}/;
4
+
5
+ export function hasScopeTemplate(value: string): boolean {
6
+ return SCOPE_TEMPLATE_RE.test(value);
7
+ }
8
+
9
+ export function evaluateScopeExpression(
10
+ expression: string,
11
+ scope: unknown
12
+ ): unknown {
13
+ const trimmed = expression.trim();
14
+ if (!trimmed) return undefined;
15
+ try {
16
+ const fn = new Function("scope", `"use strict"; return (${trimmed});`);
17
+ return fn(scope);
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ export function evaluateScopeTemplate(
24
+ template: string,
25
+ scope: unknown
26
+ ): string {
27
+ if (!hasScopeTemplate(template)) return template;
28
+ return template.replace(/\{([^}]+)\}/g, (_, rawExpr: string) => {
29
+ const value = evaluateScopeExpression(rawExpr, scope);
30
+ if (value === null || value === undefined) return "";
31
+ if (typeof value === "string") return value;
32
+ if (typeof value === "number" || typeof value === "boolean") {
33
+ return String(value);
34
+ }
35
+ try {
36
+ return JSON.stringify(value);
37
+ } catch {
38
+ return String(value);
39
+ }
40
+ });
41
+ }
42
+
43
+ function resolveScopedValue<T>(value: T, scope: unknown): T {
44
+ if (typeof value === "string" && hasScopeTemplate(value)) {
45
+ return evaluateScopeTemplate(value, scope) as T;
46
+ }
47
+ if (Array.isArray(value)) {
48
+ return value.map((item) => resolveScopedValue(item, scope)) as T;
49
+ }
50
+ if (value && typeof value === "object") {
51
+ const out: Record<string, unknown> = {};
52
+ for (const [key, nested] of Object.entries(value)) {
53
+ out[key] = resolveScopedValue(nested, scope);
54
+ }
55
+ return out as T;
56
+ }
57
+ return value;
58
+ }
59
+
60
+ export function resolvePanelElementScope(
61
+ element: PanelElement,
62
+ scope: unknown | undefined
63
+ ): PanelElement {
64
+ if (scope === undefined) return element;
65
+ return resolveScopedValue(structuredClone(element), scope);
66
+ }
67
+
68
+ export function formatViewElementScope(scope: unknown): string {
69
+ if (scope === undefined) return "";
70
+ try {
71
+ return JSON.stringify(scope, null, 2);
72
+ } catch {
73
+ return String(scope);
74
+ }
75
+ }