@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,316 @@
1
+ import type { EChartsOption } from "echarts";
2
+ import type { PanelChartConfig, PanelElement } from "../types";
3
+
4
+ const DEFAULT_CHART_VALUES = [12, 18, 9, 24];
5
+
6
+ export function getChartValuesDisplayText(chart?: PanelChartConfig): string {
7
+ if (chart?.valuesText !== undefined) return chart.valuesText;
8
+ return (chart?.values ?? []).join(",");
9
+ }
10
+
11
+ export function parseChartValuesFromText(
12
+ text: string | undefined,
13
+ fallback: number[] = DEFAULT_CHART_VALUES
14
+ ): number[] {
15
+ if (text === undefined) return fallback;
16
+ const trimmed = text.trim();
17
+ if (!trimmed) return fallback;
18
+ const parts = trimmed.split(",").map((s) => s.trim()).filter(Boolean);
19
+ if (!parts.length) return fallback;
20
+ return parts.map((s) => {
21
+ const n = Number(s);
22
+ return Number.isFinite(n) ? n : 0;
23
+ });
24
+ }
25
+
26
+ export function resolveChartValues(element: PanelElement): number[] {
27
+ const chart = element.chart;
28
+ if (chart?.valuesText !== undefined) {
29
+ const parsed = parseChartValuesFromText(chart.valuesText, []);
30
+ return parsed.length ? parsed : DEFAULT_CHART_VALUES;
31
+ }
32
+ return chart?.values?.length ? chart.values : DEFAULT_CHART_VALUES;
33
+ }
34
+
35
+ export const CHART_TYPES = new Set([
36
+ "bar",
37
+ "line",
38
+ "pie",
39
+ "area",
40
+ "scatter",
41
+ "radar",
42
+ "gauge",
43
+ "funnel",
44
+ ]);
45
+
46
+ type ChartType =
47
+ | "bar"
48
+ | "line"
49
+ | "pie"
50
+ | "area"
51
+ | "scatter"
52
+ | "radar"
53
+ | "gauge"
54
+ | "funnel";
55
+
56
+ function deepMerge<T extends Record<string, any>>(base: T, patch?: Record<string, unknown>): T {
57
+ if (!patch) return base;
58
+ const next: Record<string, any> = { ...base };
59
+ for (const [key, value] of Object.entries(patch)) {
60
+ if (
61
+ value &&
62
+ typeof value === "object" &&
63
+ !Array.isArray(value) &&
64
+ next[key] &&
65
+ typeof next[key] === "object" &&
66
+ !Array.isArray(next[key])
67
+ ) {
68
+ next[key] = deepMerge(next[key], value as Record<string, unknown>);
69
+ } else {
70
+ next[key] = value;
71
+ }
72
+ }
73
+ return next as T;
74
+ }
75
+
76
+ export function buildChartOption(element: PanelElement): EChartsOption {
77
+ const chartType = (element.materialType ?? "") as ChartType;
78
+ const labels = element.chart?.labels?.length ? element.chart.labels : ["A", "B", "C", "D"];
79
+ const values = resolveChartValues(element);
80
+ const color = element.chart?.color || "#3b82f6";
81
+ const gradientFrom = element.chart?.gradientFrom || color;
82
+ const gradientTo = element.chart?.gradientTo || "#22d3ee";
83
+ const gradientDirection = element.chart?.gradientDirection ?? "to-right";
84
+ const useGradient = element.chart?.colorMode === "gradient";
85
+ const gradientMeta =
86
+ gradientDirection === "to-bottom"
87
+ ? { x: 0, y: 0, x2: 0, y2: 1 }
88
+ : gradientDirection === "to-bottom-right"
89
+ ? { x: 0, y: 0, x2: 1, y2: 1 }
90
+ : gradientDirection === "to-top-right"
91
+ ? { x: 0, y: 1, x2: 1, y2: 0 }
92
+ : { x: 0, y: 0, x2: 1, y2: 0 };
93
+ const chartColor = useGradient
94
+ ? ({
95
+ type: "linear",
96
+ ...gradientMeta,
97
+ colorStops: [
98
+ { offset: 0, color: gradientFrom },
99
+ { offset: 1, color: gradientTo },
100
+ ],
101
+ } as const)
102
+ : color;
103
+ const title = element.chart?.title ?? "";
104
+ const xAxisName = element.chart?.xAxisName ?? "";
105
+ const yAxisName = element.chart?.yAxisName ?? "";
106
+ const xAxisTickShow = element.chart?.xAxisTickShow ?? true;
107
+ const yAxisTickShow = element.chart?.yAxisTickShow ?? true;
108
+ const xAxisTickColor = element.chart?.xAxisTickColor ?? "#94a3b8";
109
+ const yAxisTickColor = element.chart?.yAxisTickColor ?? "#94a3b8";
110
+ const xAxisSplitLineShow = element.chart?.xAxisSplitLineShow ?? false;
111
+ const yAxisSplitLineShow = element.chart?.yAxisSplitLineShow ?? true;
112
+ const xAxisSplitLineColor = element.chart?.xAxisSplitLineColor ?? "#e2e8f0";
113
+ const yAxisSplitLineColor = element.chart?.yAxisSplitLineColor ?? "#e2e8f0";
114
+ const xAxisLabelColor = element.chart?.xAxisLabelColor ?? "#64748b";
115
+ const yAxisLabelColor = element.chart?.yAxisLabelColor ?? "#64748b";
116
+ const xAxisLabelFontSize = element.chart?.xAxisLabelFontSize ?? 10;
117
+ const yAxisLabelFontSize = element.chart?.yAxisLabelFontSize ?? 10;
118
+ const xAxisLabelAutoEllipsis = element.chart?.xAxisLabelAutoEllipsis ?? false;
119
+ const yAxisLabelAutoEllipsis = element.chart?.yAxisLabelAutoEllipsis ?? false;
120
+ const tooltipShow = element.chart?.tooltipShow ?? true;
121
+ const tooltipTrigger = element.chart?.tooltipTrigger ?? (chartType === "pie" || chartType === "funnel" || chartType === "scatter" ? "item" : "axis");
122
+ const tooltipBackgroundColor = element.chart?.tooltipBackgroundColor ?? "#0f172a";
123
+ const tooltipTextColor = element.chart?.tooltipTextColor ?? "#f8fafc";
124
+ const tooltipFormatter = element.chart?.tooltipFormatter;
125
+ const tooltipOption = {
126
+ show: tooltipShow,
127
+ trigger: tooltipTrigger,
128
+ backgroundColor: tooltipBackgroundColor,
129
+ textStyle: { color: tooltipTextColor },
130
+ formatter: tooltipFormatter || undefined,
131
+ };
132
+
133
+ if (chartType === "pie") {
134
+ const inner = element.chart?.pieInnerRadius ?? 30;
135
+ const outer = element.chart?.pieOuterRadius ?? 65;
136
+ const baseOption: EChartsOption = {
137
+ animation: false,
138
+ title: { text: title, left: "center", top: 6, textStyle: { fontSize: 12 } },
139
+ color: [chartColor as any],
140
+ tooltip: tooltipOption,
141
+ series: [
142
+ {
143
+ type: "pie",
144
+ radius: [`${inner}%`, `${outer}%`],
145
+ center: ["50%", "58%"],
146
+ label: { fontSize: 10 },
147
+ data: labels.map((name, i) => ({ name, value: values[i] ?? 0 })),
148
+ },
149
+ ],
150
+ };
151
+ return deepMerge(baseOption as Record<string, any>, element.chart?.option) as EChartsOption;
152
+ }
153
+
154
+ if (chartType === "gauge") {
155
+ const gaugeValue = values[0] ?? 0;
156
+ const baseOption: EChartsOption = {
157
+ animation: false,
158
+ title: { text: title, left: "center", top: 6, textStyle: { fontSize: 12 } },
159
+ tooltip: tooltipOption,
160
+ series: [
161
+ {
162
+ type: "gauge",
163
+ min: 0,
164
+ max: 100,
165
+ progress: { show: true, width: 10 },
166
+ axisLine: { lineStyle: { width: 10 } },
167
+ detail: { valueAnimation: false, formatter: "{value}%" },
168
+ data: [{ value: gaugeValue, name: title }],
169
+ itemStyle: { color: chartColor as any },
170
+ },
171
+ ],
172
+ };
173
+ return deepMerge(baseOption as Record<string, any>, element.chart?.option) as EChartsOption;
174
+ }
175
+
176
+ if (chartType === "radar") {
177
+ const indicator = labels.map((name) => ({ name, max: 100 }));
178
+ const baseOption: EChartsOption = {
179
+ animation: false,
180
+ title: { text: title, left: "center", top: 6, textStyle: { fontSize: 12 } },
181
+ tooltip: tooltipOption,
182
+ radar: { indicator, radius: "60%", center: ["50%", "58%"] },
183
+ series: [
184
+ {
185
+ type: "radar",
186
+ data: [{ value: values, name: title }],
187
+ areaStyle: { opacity: 0.25 },
188
+ lineStyle: { color: chartColor as any },
189
+ itemStyle: { color: chartColor as any },
190
+ },
191
+ ],
192
+ };
193
+ return deepMerge(baseOption as Record<string, any>, element.chart?.option) as EChartsOption;
194
+ }
195
+
196
+ if (chartType === "funnel") {
197
+ const baseOption: EChartsOption = {
198
+ animation: false,
199
+ title: { text: title, left: "center", top: 6, textStyle: { fontSize: 12 } },
200
+ tooltip: tooltipOption,
201
+ series: [
202
+ {
203
+ type: "funnel",
204
+ top: 28,
205
+ left: "10%",
206
+ width: "80%",
207
+ height: "65%",
208
+ sort: "descending",
209
+ data: labels.map((name, i) => ({ name, value: values[i] ?? 0 })),
210
+ label: { fontSize: 10 },
211
+ itemStyle: { color: chartColor as any },
212
+ },
213
+ ],
214
+ };
215
+ return deepMerge(baseOption as Record<string, any>, element.chart?.option) as EChartsOption;
216
+ }
217
+
218
+ if (chartType === "scatter") {
219
+ const baseOption: EChartsOption = {
220
+ animation: false,
221
+ title: { text: title, left: 8, top: 6, textStyle: { fontSize: 12 } },
222
+ grid: { left: 28, right: 10, top: 30, bottom: 20 },
223
+ xAxis: {
224
+ type: "value",
225
+ name: xAxisName,
226
+ nameTextStyle: { fontSize: 10 },
227
+ axisLabel: {
228
+ fontSize: xAxisLabelFontSize,
229
+ color: xAxisLabelColor,
230
+ overflow: xAxisLabelAutoEllipsis ? "truncate" : undefined,
231
+ hideOverlap: xAxisLabelAutoEllipsis,
232
+ width: xAxisLabelAutoEllipsis ? 80 : undefined,
233
+ },
234
+ axisTick: { show: xAxisTickShow, lineStyle: { color: xAxisTickColor } },
235
+ splitLine: { show: xAxisSplitLineShow, lineStyle: { color: xAxisSplitLineColor } },
236
+ },
237
+ yAxis: {
238
+ type: "value",
239
+ name: yAxisName,
240
+ nameTextStyle: { fontSize: 10 },
241
+ axisLabel: {
242
+ fontSize: yAxisLabelFontSize,
243
+ color: yAxisLabelColor,
244
+ overflow: yAxisLabelAutoEllipsis ? "truncate" : undefined,
245
+ hideOverlap: yAxisLabelAutoEllipsis,
246
+ width: yAxisLabelAutoEllipsis ? 80 : undefined,
247
+ },
248
+ axisTick: { show: yAxisTickShow, lineStyle: { color: yAxisTickColor } },
249
+ splitLine: { show: yAxisSplitLineShow, lineStyle: { color: yAxisSplitLineColor } },
250
+ },
251
+ tooltip: tooltipOption,
252
+ series: [
253
+ {
254
+ type: "scatter",
255
+ data: values.map((v, i) => [i + 1, v]),
256
+ itemStyle: { color: chartColor as any },
257
+ symbolSize: 10,
258
+ },
259
+ ],
260
+ };
261
+ return deepMerge(baseOption as Record<string, any>, element.chart?.option) as EChartsOption;
262
+ }
263
+
264
+ const seriesType = chartType === "area" ? "line" : chartType;
265
+ const baseOption: EChartsOption = {
266
+ animation: false,
267
+ title: { text: title, left: 8, top: 6, textStyle: { fontSize: 12 } },
268
+ grid: { left: 28, right: 10, top: 30, bottom: 20 },
269
+ xAxis: {
270
+ type: "category",
271
+ name: xAxisName,
272
+ nameTextStyle: { fontSize: 10 },
273
+ data: labels,
274
+ axisLabel: {
275
+ fontSize: xAxisLabelFontSize,
276
+ color: xAxisLabelColor,
277
+ overflow: xAxisLabelAutoEllipsis ? "truncate" : undefined,
278
+ hideOverlap: xAxisLabelAutoEllipsis,
279
+ width: xAxisLabelAutoEllipsis ? 80 : undefined,
280
+ },
281
+ axisTick: { show: xAxisTickShow, lineStyle: { color: xAxisTickColor } },
282
+ splitLine: { show: xAxisSplitLineShow, lineStyle: { color: xAxisSplitLineColor } },
283
+ },
284
+ yAxis: {
285
+ type: "value",
286
+ name: yAxisName,
287
+ nameTextStyle: { fontSize: 10 },
288
+ axisLabel: {
289
+ fontSize: yAxisLabelFontSize,
290
+ color: yAxisLabelColor,
291
+ overflow: yAxisLabelAutoEllipsis ? "truncate" : undefined,
292
+ hideOverlap: yAxisLabelAutoEllipsis,
293
+ width: yAxisLabelAutoEllipsis ? 80 : undefined,
294
+ },
295
+ axisTick: { show: yAxisTickShow, lineStyle: { color: yAxisTickColor } },
296
+ splitLine: { show: yAxisSplitLineShow, lineStyle: { color: yAxisSplitLineColor } },
297
+ },
298
+ tooltip: tooltipOption,
299
+ series: [
300
+ {
301
+ type: seriesType,
302
+ data: values,
303
+ smooth:
304
+ chartType === "line" || chartType === "area"
305
+ ? (element.chart?.smooth ?? true)
306
+ : undefined,
307
+ barWidth: chartType === "bar" ? (element.chart?.barWidth ?? 24) : undefined,
308
+ areaStyle: chartType === "area" ? { opacity: 0.25 } : undefined,
309
+ itemStyle: { color: chartColor as any },
310
+ lineStyle: { color: chartColor as any },
311
+ },
312
+ ],
313
+ };
314
+ return deepMerge(baseOption as Record<string, any>, element.chart?.option) as EChartsOption;
315
+ }
316
+
@@ -0,0 +1,189 @@
1
+ import type { PanelElement } from "../types";
2
+
3
+ export function getGridSlotLayout(grid: PanelElement) {
4
+ const rows = Math.max(1, Math.floor(grid.gridRows ?? 2));
5
+ const cols = Math.max(1, Math.floor(grid.gridCols ?? 3));
6
+ const gap = Math.max(0, grid.gridGap ?? 8);
7
+ const padding = Math.max(0, grid.gridPadding ?? 10);
8
+ const innerWidth = Math.max(1, grid.width - padding * 2);
9
+ const innerHeight = Math.max(1, grid.height - padding * 2);
10
+ const cellWidth = Math.max(1, (innerWidth - gap * (cols - 1)) / cols);
11
+ const cellHeight = Math.max(1, (innerHeight - gap * (rows - 1)) / rows);
12
+ const slots: Array<{ index: number; x: number; y: number; width: number; height: number }> = [];
13
+ for (let r = 0; r < rows; r++) {
14
+ for (let c = 0; c < cols; c++) {
15
+ const x = grid.x + padding + c * (cellWidth + gap);
16
+ const y = grid.y + padding + r * (cellHeight + gap);
17
+ slots.push({
18
+ index: r * cols + c,
19
+ x,
20
+ y,
21
+ width: cellWidth,
22
+ height: cellHeight,
23
+ });
24
+ }
25
+ }
26
+ return { slots, rows, cols, cellWidth, cellHeight, gap };
27
+ }
28
+
29
+ export function getGridChildSpanRect(grid: PanelElement, slotIndex: number, colSpan: number, rowSpan: number) {
30
+ const { rows, cols, cellWidth, cellHeight, gap, slots } = getGridSlotLayout(grid);
31
+ const total = rows * cols;
32
+ const safeIndex = Math.max(0, Math.min(total - 1, Math.floor(slotIndex || 0)));
33
+ const baseRow = Math.floor(safeIndex / cols);
34
+ const baseCol = safeIndex % cols;
35
+ const safeColSpan = Math.max(1, Math.min(cols - baseCol, Math.floor(colSpan || 1)));
36
+ const safeRowSpan = Math.max(1, Math.min(rows - baseRow, Math.floor(rowSpan || 1)));
37
+ const baseSlot = slots[safeIndex];
38
+ const width = safeColSpan * cellWidth + (safeColSpan - 1) * gap;
39
+ const height = safeRowSpan * cellHeight + (safeRowSpan - 1) * gap;
40
+ return {
41
+ index: safeIndex,
42
+ x: baseSlot?.x ?? grid.x,
43
+ y: baseSlot?.y ?? grid.y,
44
+ width: Math.max(1, width),
45
+ height: Math.max(1, height),
46
+ colSpan: safeColSpan,
47
+ rowSpan: safeRowSpan,
48
+ };
49
+ }
50
+
51
+ export function inferSpanBySize(size: number, cellSize: number, gap: number, maxSpan: number) {
52
+ const safeMax = Math.max(1, Math.floor(maxSpan));
53
+ const unit = Math.max(1, cellSize + gap);
54
+ const ratio = (Math.max(1, size) + gap) / unit;
55
+ const whole = Math.floor(ratio);
56
+ const fraction = ratio - whole;
57
+ const promoted = fraction >= 0.72 ? whole + 1 : Math.max(1, whole);
58
+ return Math.max(1, Math.min(safeMax, promoted));
59
+ }
60
+
61
+ export function getGridSpanCellsForSnap(
62
+ rows: number,
63
+ cols: number,
64
+ slotIndex: number,
65
+ colSpan: number,
66
+ rowSpan: number
67
+ ) {
68
+ const total = rows * cols;
69
+ const safeIndex = Math.max(0, Math.min(total - 1, Math.floor(slotIndex || 0)));
70
+ const startRow = Math.floor(safeIndex / cols);
71
+ const startCol = safeIndex % cols;
72
+ const safeColSpan = Math.max(1, Math.min(cols - startCol, Math.floor(colSpan || 1)));
73
+ const safeRowSpan = Math.max(1, Math.min(rows - startRow, Math.floor(rowSpan || 1)));
74
+ const indices: number[] = [];
75
+ for (let r = startRow; r < startRow + safeRowSpan; r++) {
76
+ for (let c = startCol; c < startCol + safeColSpan; c++) {
77
+ indices.push(r * cols + c);
78
+ }
79
+ }
80
+ return {
81
+ safeIndex,
82
+ startRow,
83
+ startCol,
84
+ colSpan: safeColSpan,
85
+ rowSpan: safeRowSpan,
86
+ indices,
87
+ };
88
+ }
89
+
90
+ export function getOccupiedSlotsForGrid(
91
+ elementsById: Map<string, PanelElement>,
92
+ gridId: string,
93
+ excludeId: string | undefined,
94
+ layerId: string
95
+ ) {
96
+ const occupied = new Set<number>();
97
+ const grid = elementsById.get(gridId);
98
+ if (!grid || grid.materialType !== "grid") return occupied;
99
+ const { rows, cols } = getGridSlotLayout(grid);
100
+ for (const el of elementsById.values()) {
101
+ if (excludeId && el.id === excludeId) continue;
102
+ if (el.layerId !== layerId) continue;
103
+ if (el.parentGridId !== gridId) continue;
104
+ if (el.gridSlotIndex === undefined) continue;
105
+ const span = getGridSpanCellsForSnap(rows, cols, el.gridSlotIndex, el.gridColSpan ?? 1, el.gridRowSpan ?? 1);
106
+ span.indices.forEach((idx) => occupied.add(idx));
107
+ }
108
+ return occupied;
109
+ }
110
+
111
+ /** 新建节点落点吸附到指定图层上的网格(与 Moveable 吸附规则对齐) */
112
+ export function computeSnapPatchForNewElementOnLayer(
113
+ elementsById: Map<string, PanelElement>,
114
+ layerId: string,
115
+ x: number,
116
+ y: number,
117
+ width: number,
118
+ height: number,
119
+ excludeElementId?: string
120
+ ): Partial<PanelElement> {
121
+ const centerX = x + width / 2;
122
+ const centerY = y + height / 2;
123
+ let nearest: {
124
+ gridId: string;
125
+ slotIndex: number;
126
+ slotX: number;
127
+ slotY: number;
128
+ width: number;
129
+ height: number;
130
+ colSpan: number;
131
+ rowSpan: number;
132
+ distance: number;
133
+ } | null = null;
134
+
135
+ for (const [, el] of elementsById.entries()) {
136
+ if (el.materialType !== "grid" || el.layerId !== layerId) continue;
137
+ const layout = getGridSlotLayout(el);
138
+ const { slots, cellWidth, cellHeight, rows, cols, gap } = layout;
139
+ const occupiedSlots = getOccupiedSlotsForGrid(elementsById, el.id, excludeElementId, layerId);
140
+ const threshold = Math.max(8, el.gridSnapThreshold ?? 36);
141
+ const inferredColSpan = inferSpanBySize(width, cellWidth, gap, cols);
142
+ const inferredRowSpan = inferSpanBySize(height, cellHeight, gap, rows);
143
+ const movingColSpan = Math.max(1, inferredColSpan);
144
+ const movingRowSpan = Math.max(1, inferredRowSpan);
145
+ const insideGridBounds =
146
+ centerX >= el.x &&
147
+ centerX <= el.x + Math.max(1, el.width) &&
148
+ centerY >= el.y &&
149
+ centerY <= el.y + Math.max(1, el.height);
150
+
151
+ for (const slot of slots) {
152
+ const spanCells = getGridSpanCellsForSnap(rows, cols, slot.index, movingColSpan, movingRowSpan);
153
+ if (spanCells.colSpan !== movingColSpan || spanCells.rowSpan !== movingRowSpan) continue;
154
+ const blocked = spanCells.indices.some((idx) => occupiedSlots.has(idx));
155
+ if (blocked) continue;
156
+ const slotCenterX = slot.x + cellWidth / 2;
157
+ const slotCenterY = slot.y + cellHeight / 2;
158
+ const distance = Math.hypot(slotCenterX - centerX, slotCenterY - centerY);
159
+ if (!insideGridBounds && distance > threshold) continue;
160
+ if (!nearest || distance < nearest.distance) {
161
+ const spanWidth = spanCells.colSpan * cellWidth + (spanCells.colSpan - 1) * gap;
162
+ const spanHeight = spanCells.rowSpan * cellHeight + (spanCells.rowSpan - 1) * gap;
163
+ nearest = {
164
+ gridId: el.id,
165
+ slotIndex: spanCells.safeIndex,
166
+ slotX: slot.x,
167
+ slotY: slot.y,
168
+ width: spanWidth,
169
+ height: spanHeight,
170
+ colSpan: spanCells.colSpan,
171
+ rowSpan: spanCells.rowSpan,
172
+ distance,
173
+ };
174
+ }
175
+ }
176
+ }
177
+
178
+ if (!nearest) return {};
179
+ return {
180
+ x: nearest.slotX,
181
+ y: nearest.slotY,
182
+ parentGridId: nearest.gridId,
183
+ gridSlotIndex: nearest.slotIndex,
184
+ gridColSpan: nearest.colSpan,
185
+ gridRowSpan: nearest.rowSpan,
186
+ width: nearest.width,
187
+ height: nearest.height,
188
+ };
189
+ }
@@ -0,0 +1,142 @@
1
+ import type { Node, State } from "../../../../rx-store/src/types";
2
+ import type { PanelElement, PanelLayer } from "../types";
3
+ import { DEFAULT_LAYER_ID, DEFAULT_LAYER } from "./panelElementDefaults";
4
+ import { isPanelElementNode } from "./panelElementNodes";
5
+
6
+ /**
7
+ * 沿 mappingSourceNodeId 追溯到「源画布节点」id。
8
+ * 多层映射 / 中间克隆指向时仍与同源簇对齐;用于批量同步与节点树一致。
9
+ */
10
+ export function canonicalMappingFamilyRootId(
11
+ elementById: Map<string, PanelElement>,
12
+ nodeId: string
13
+ ): string {
14
+ const visited = new Set<string>();
15
+ let cur = nodeId;
16
+ for (;;) {
17
+ if (visited.has(cur)) return cur;
18
+ visited.add(cur);
19
+ const el = elementById.get(cur);
20
+ const next = el?.mappingSourceNodeId;
21
+ if (!next || next === cur) return cur;
22
+ cur = next;
23
+ }
24
+ }
25
+
26
+ /** 映射图层克隆节点:基准图层上的节点 id → 该映射图层上与之源一致的节点 id */
27
+ export function findCloneIdForSourceNodeOnMappingLayer(
28
+ sourceElementId: string,
29
+ mappingLayerId: string,
30
+ elementsById: Map<string, PanelElement>
31
+ ): string | undefined {
32
+ const root = canonicalMappingFamilyRootId(elementsById, sourceElementId);
33
+ for (const el of elementsById.values()) {
34
+ if (el.layerId !== mappingLayerId) continue;
35
+ if (!el.mappingSourceNodeId) continue;
36
+ if (canonicalMappingFamilyRootId(elementsById, el.mappingSourceNodeId) !== root) continue;
37
+ return el.id;
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ /** 克隆网格 id → 逻辑上的「源图层网格」同源 id(跨多层映射);非法/缺失父节点时返回 undefined */
43
+ export function logicalGridParentIdFromConcrete(
44
+ concreteParentGridId: string | undefined,
45
+ elementById: Map<string, PanelElement>
46
+ ): string | undefined {
47
+ if (!concreteParentGridId) return undefined;
48
+ const g = elementById.get(concreteParentGridId);
49
+ if (!g || g.materialType !== "grid") return undefined;
50
+ return canonicalMappingFamilyRootId(elementById, g.id);
51
+ }
52
+
53
+ /** 逻辑源网格同源 id → 在指定图层上的那张网格实例 id */
54
+ export function concreteGridParentIdForLayer(
55
+ logicalSourceGridId: string | undefined,
56
+ targetLayerId: string,
57
+ elements: Iterable<PanelElement>
58
+ ): string | undefined {
59
+ if (!logicalSourceGridId) return undefined;
60
+ const byId = new Map<string, PanelElement>();
61
+ for (const el of elements) byId.set(el.id, el);
62
+ for (const el of elements) {
63
+ if (el.layerId !== targetLayerId || el.materialType !== "grid") continue;
64
+ if (canonicalMappingFamilyRootId(byId, el.id) === logicalSourceGridId) return el.id;
65
+ }
66
+ return undefined;
67
+ }
68
+
69
+ /** 打开映射图层时:把选中节点在同图层内的网格子树一并纳入,避免出现「父网格+兄弟节点已克隆、中间嵌套网格丢失」的断层结构 */
70
+ export function expandMappingSeedsWithGridDescendants(nodeById: Map<string, PanelElement>, seedIds: string[]): string[] {
71
+ const expanded = new Set<string>();
72
+ const queue: string[] = [];
73
+ for (const id of seedIds) {
74
+ if (!id || !nodeById.has(id)) continue;
75
+ expanded.add(id);
76
+ queue.push(id);
77
+ }
78
+ while (queue.length > 0) {
79
+ const id = queue.shift()!;
80
+ const anchor = nodeById.get(id);
81
+ const layerId = anchor?.layerId;
82
+ for (const el of nodeById.values()) {
83
+ if (el.parentGridId !== id) continue;
84
+ if (layerId !== undefined && el.layerId !== layerId) continue;
85
+ if (expanded.has(el.id)) continue;
86
+ expanded.add(el.id);
87
+ queue.push(el.id);
88
+ }
89
+ }
90
+ return [...expanded];
91
+ }
92
+
93
+ export function removeMappingLayersBySourceIds(
94
+ draft: State,
95
+ sourceIds: Set<string>,
96
+ fallbackActiveLayerId: string = DEFAULT_LAYER_ID
97
+ ) {
98
+ if (sourceIds.size === 0) return;
99
+ const nodes = draft.root.children ?? [];
100
+ const mappingLayerIds = new Set<string>();
101
+ nodes.forEach((n) => {
102
+ if (!isPanelElementNode(n) || !n.props) return;
103
+ const props = n.props as PanelElement;
104
+ if (!props.mappingSourceNodeId) return;
105
+ if (!sourceIds.has(props.mappingSourceNodeId)) return;
106
+ mappingLayerIds.add(props.layerId);
107
+ });
108
+ if (mappingLayerIds.size === 0) return;
109
+ removeLayersByIds(draft, mappingLayerIds, fallbackActiveLayerId);
110
+ }
111
+
112
+ export function removeLayersByIds(draft: State, layerIds: Set<string>, fallbackActiveLayerId: string = DEFAULT_LAYER_ID) {
113
+ if (layerIds.size === 0) return;
114
+ const nodes = draft.root.children ?? [];
115
+
116
+ draft.root.children = nodes.filter((n) => {
117
+ if (!isPanelElementNode(n) || !n.props) return true;
118
+ const props = n.props as PanelElement;
119
+ return !layerIds.has(props.layerId);
120
+ });
121
+
122
+ draft.variables = draft.variables ?? {};
123
+ const layers = (draft.variables.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
124
+ draft.variables.layers = layers.filter((layer) => !layerIds.has(layer.id));
125
+ const activeLayerId = (draft.variables.activeLayerId as string | undefined) ?? fallbackActiveLayerId;
126
+ if (layerIds.has(activeLayerId)) {
127
+ draft.variables.activeLayerId =
128
+ (draft.variables.layers[0] as PanelLayer | undefined)?.id ?? fallbackActiveLayerId;
129
+ }
130
+ }
131
+
132
+ export function getMaxZIndexByLayer(nodes: Node[], layerId: string): number {
133
+ let maxZ = 0;
134
+ nodes.forEach((n) => {
135
+ if (!isPanelElementNode(n) || !n.props) return;
136
+ const props = n.props as PanelElement;
137
+ if (props.layerId !== layerId) return;
138
+ const z = typeof props.zIndex === "number" ? props.zIndex : 1;
139
+ if (z > maxZ) maxZ = z;
140
+ });
141
+ return maxZ;
142
+ }