@arronqzy/react-view 1.0.3 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/styles.css +555 -15
  2. package/package.json +14 -7
  3. package/readme.md +74 -5
  4. package/src/core/template.tsx +12 -2
  5. package/src/panel/ReactViewOnlinePreview.tsx +18 -8
  6. package/src/panel/ReactViewPanel.tsx +403 -177
  7. package/src/panel/ai/AssistantChatPanel.tsx +895 -0
  8. package/src/panel/ai/AssistantMarkdown.tsx +256 -0
  9. package/src/panel/ai/applyAssistantAction.ts +652 -0
  10. package/src/panel/ai/revealConfigField.ts +137 -0
  11. package/src/panel/components/ConfigHintIcon.tsx +50 -0
  12. package/src/panel/components/ElementsLayer.tsx +39 -22
  13. package/src/panel/components/MaterialSidebar.tsx +157 -109
  14. package/src/panel/components/MoveableLayer.tsx +75 -61
  15. package/src/panel/components/PanelCanvas.tsx +81 -64
  16. package/src/panel/components/PanelConfigSidebar.tsx +471 -448
  17. package/src/panel/components/SelectLayer.tsx +1 -0
  18. package/src/panel/components/ViewElementScopePanel.tsx +13 -12
  19. package/src/panel/components/WorkspaceConfigSidebar.tsx +5 -2
  20. package/src/panel/components/WorkspaceProjectNav.tsx +25 -14
  21. package/src/panel/components/WorkspaceStageSplit.tsx +3 -1
  22. package/src/panel/components/scope-config/ScopeConfigContext.tsx +9 -0
  23. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +242 -0
  24. package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +12 -0
  25. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +6 -3
  26. package/src/panel/components/table/ConditionEditor.tsx +205 -0
  27. package/src/panel/components/table/JsonCodeEditor.tsx +115 -0
  28. package/src/panel/components/table/PanelConfigTableSection.tsx +2192 -0
  29. package/src/panel/components/table/TableNodeContent.tsx +544 -0
  30. package/src/panel/constants/messages.ts +76 -34
  31. package/src/panel/hooks/usePanelElements.ts +64 -52
  32. package/src/panel/hooks/useRafThrottledScroll.ts +30 -0
  33. package/src/panel/hooks/useWorkspaceProjects.ts +11 -8
  34. package/src/panel/types.ts +6 -0
  35. package/src/panel/utils/chartOptionBuilder.ts +12 -1
  36. package/src/panel/utils/panelElementDefaults.ts +69 -39
  37. package/src/panel/utils/scope-field-labels.ts +51 -37
  38. package/src/panel/utils/scope-template-chart.ts +92 -0
  39. package/src/panel/utils/scope-template-preview.ts +128 -0
  40. package/src/panel/utils/scope-template-spread.ts +252 -0
  41. package/src/panel/utils/scope-template-warnings.ts +131 -11
  42. package/src/panel/utils/scope-template.ts +105 -3
  43. package/src/tailwind.css +36 -15
@@ -1,43 +1,57 @@
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: "几何脚本",
1
+ import type { TranslateFn } from "@arronqzy/i18n";
2
+
3
+ const FIELD_KEY_MAP: Record<string, string> = {
4
+ name: "panel.scope.fieldName",
5
+ textHtml: "panel.scope.fieldTextHtml",
6
+ textFontFamily: "panel.scope.fieldTextFontFamily",
7
+ textFontWeight: "panel.scope.fieldTextFontWeight",
8
+ textColor: "panel.scope.fieldTextColor",
9
+ "chart.title": "panel.scope.fieldChartTitle",
10
+ "chart.color": "panel.scope.fieldChartColor",
11
+ "chart.gradientFrom": "panel.scope.fieldChartGradientFrom",
12
+ "chart.gradientTo": "panel.scope.fieldChartGradientTo",
13
+ "chart.xAxisName": "panel.scope.fieldChartXAxisName",
14
+ "chart.yAxisName": "panel.scope.fieldChartYAxisName",
15
+ "chart.xAxisTickColor": "panel.scope.fieldChartXAxisTickColor",
16
+ "chart.yAxisTickColor": "panel.scope.fieldChartYAxisTickColor",
17
+ "chart.xAxisSplitLineColor": "panel.scope.fieldChartXAxisSplitLineColor",
18
+ "chart.yAxisSplitLineColor": "panel.scope.fieldChartYAxisSplitLineColor",
19
+ "chart.xAxisLabelColor": "panel.scope.fieldChartXAxisLabelColor",
20
+ "chart.yAxisLabelColor": "panel.scope.fieldChartYAxisLabelColor",
21
+ "chart.tooltipBackgroundColor": "panel.scope.fieldChartTooltipBackgroundColor",
22
+ "chart.tooltipTextColor": "panel.scope.fieldChartTooltipTextColor",
23
+ "chart.tooltipFormatter": "panel.scope.fieldChartTooltipFormatter",
24
+ "chart.labelsText": "panel.scope.fieldChartLabelsText",
25
+ "chart.valuesText": "panel.scope.fieldChartValuesText",
26
+ "style.backgroundColor": "panel.scope.fieldStyleBackgroundColor",
27
+ "style.backgroundImage": "panel.scope.fieldStyleBackgroundImage",
28
+ "style.backgroundImageRemoteUrl": "panel.scope.fieldStyleBackgroundImageRemoteUrl",
29
+ "style.backgroundSize": "panel.scope.fieldStyleBackgroundSize",
30
+ "style.backgroundPosition": "panel.scope.fieldStyleBackgroundPosition",
31
+ "style.borderColor": "panel.scope.fieldStyleBorderColor",
32
+ audioSrc: "panel.scope.fieldAudioSrc",
33
+ audioRemoteUrl: "panel.scope.fieldAudioRemoteUrl",
34
+ videoSrc: "panel.scope.fieldVideoSrc",
35
+ videoRemoteUrl: "panel.scope.fieldVideoRemoteUrl",
36
+ geometryColor: "panel.scope.fieldGeometryColor",
37
+ geometryScript: "panel.scope.fieldGeometryScript",
35
38
  };
36
39
 
37
- export function resolveScopeFieldLabel(fieldId: string): string {
38
- if (LABELS[fieldId]) return LABELS[fieldId];
40
+ export function getScopeFieldLabels(t: TranslateFn): Record<string, string> {
41
+ const labels: Record<string, string> = {};
42
+ for (const [fieldId, key] of Object.entries(FIELD_KEY_MAP)) {
43
+ labels[fieldId] = t(key);
44
+ }
45
+ return labels;
46
+ }
47
+
48
+ export function resolveScopeFieldLabel(fieldId: string, t?: TranslateFn): string {
49
+ if (t) {
50
+ const key = FIELD_KEY_MAP[fieldId];
51
+ if (key) return t(key);
52
+ }
39
53
  const leaf = fieldId.split(".").pop() ?? fieldId;
40
- return LABELS[fieldId] ?? leaf;
54
+ return leaf;
41
55
  }
42
56
 
43
57
  export function scopeFieldDomId(fieldId: string): string {
@@ -0,0 +1,92 @@
1
+ import type { PanelChartConfig } from "../types";
2
+ import { hasScopeTemplate, evaluateScopeTemplate } from "./scope-template";
3
+ import {
4
+ evaluateSpreadScopeExpressionValues,
5
+ extractSpreadScopeExpression,
6
+ isSpreadOnlyScopeTemplate,
7
+ } from "./scope-template-spread";
8
+
9
+ function splitCommaSeparatedLabels(text: string): string[] {
10
+ return text
11
+ .split(",")
12
+ .map((part) => part.trim())
13
+ .filter(Boolean);
14
+ }
15
+
16
+ function materializeSpreadTemplate(template: string, scope: unknown): string[] {
17
+ const expression = extractSpreadScopeExpression(template);
18
+ if (!expression) return [];
19
+ const result = evaluateSpreadScopeExpressionValues(expression, scope);
20
+ return result.ok ? result.values : [];
21
+ }
22
+
23
+ function materializeLabelToken(token: string, scope: unknown): string[] {
24
+ const trimmed = token.trim();
25
+ if (!trimmed) return [];
26
+
27
+ if (isSpreadOnlyScopeTemplate(trimmed)) {
28
+ return materializeSpreadTemplate(trimmed, scope);
29
+ }
30
+
31
+ if (hasScopeTemplate(trimmed)) {
32
+ return [evaluateScopeTemplate(trimmed, scope)];
33
+ }
34
+
35
+ return [trimmed];
36
+ }
37
+
38
+ /** 将类目模版文本展开为图表所需的字符串数组 */
39
+ export function materializeChartLabelsText(
40
+ text: string,
41
+ scope: unknown
42
+ ): string[] {
43
+ const trimmed = text.trim();
44
+ if (!trimmed) return [];
45
+
46
+ if (isSpreadOnlyScopeTemplate(trimmed)) {
47
+ return materializeSpreadTemplate(trimmed, scope);
48
+ }
49
+
50
+ if (hasScopeTemplate(trimmed)) {
51
+ return splitCommaSeparatedLabels(evaluateScopeTemplate(trimmed, scope));
52
+ }
53
+
54
+ return splitCommaSeparatedLabels(trimmed);
55
+ }
56
+
57
+ /** 兼容旧数据:labels 数组中每项可含展开模版 */
58
+ export function materializeChartLabelsArray(
59
+ labels: string[],
60
+ scope: unknown
61
+ ): string[] {
62
+ const result: string[] = [];
63
+ for (const label of labels) {
64
+ result.push(...materializeLabelToken(label, scope));
65
+ }
66
+ return result;
67
+ }
68
+
69
+ /** 将数值模版文本展开为图表所需的数字数组 */
70
+ export function materializeChartValuesText(
71
+ text: string,
72
+ scope: unknown
73
+ ): number[] {
74
+ return materializeChartLabelsText(text, scope).map((part) => {
75
+ const value = Number(part);
76
+ return Number.isFinite(value) ? value : 0;
77
+ });
78
+ }
79
+
80
+ export function materializeChartLabelsFromScope(
81
+ chart: PanelChartConfig | undefined,
82
+ scope: unknown
83
+ ): string[] | undefined {
84
+ if (!chart) return undefined;
85
+ if (chart.labelsText !== undefined) {
86
+ return materializeChartLabelsText(chart.labelsText, scope);
87
+ }
88
+ if (chart.labels?.length) {
89
+ return materializeChartLabelsArray(chart.labels, scope);
90
+ }
91
+ return undefined;
92
+ }
@@ -0,0 +1,128 @@
1
+ import type { TranslateFn } from "@arronqzy/i18n";
2
+ import { tForLocale } from "@arronqzy/i18n";
3
+ import type { PanelElement } from "../types";
4
+ import {
5
+ materializeChartLabelsText,
6
+ materializeChartValuesText,
7
+ } from "./scope-template-chart";
8
+ import { hasScopeTemplate, evaluateScopeTemplate } from "./scope-template";
9
+ import { SCOPE_SPREAD_TEMPLATE_RE } from "./scope-template-spread";
10
+ import { resolveScopeFieldLabel } from "./scope-field-labels";
11
+
12
+ export type ScopeTemplateField = {
13
+ fieldId: string;
14
+ fieldLabel: string;
15
+ template: string;
16
+ };
17
+
18
+ const SKIP_FIELD_PREFIXES = new Set(["refSnapshot", "geometrySketchDataUrl"]);
19
+
20
+ function shouldSkipField(fieldId: string): boolean {
21
+ return [...SKIP_FIELD_PREFIXES].some((prefix) => fieldId.startsWith(prefix));
22
+ }
23
+
24
+ function walkElementTemplateFields(
25
+ value: unknown,
26
+ fieldId: string,
27
+ results: ScopeTemplateField[],
28
+ t: TranslateFn
29
+ ) {
30
+ if (shouldSkipField(fieldId)) return;
31
+
32
+ if (typeof value === "string") {
33
+ if (!hasScopeTemplate(value)) return;
34
+ results.push({
35
+ fieldId,
36
+ fieldLabel: resolveScopeFieldLabel(fieldId, t),
37
+ template: value,
38
+ });
39
+ return;
40
+ }
41
+
42
+ if (Array.isArray(value)) {
43
+ value.forEach((item, index) => {
44
+ walkElementTemplateFields(item, `${fieldId}.${index}`, results, t);
45
+ });
46
+ return;
47
+ }
48
+
49
+ if (value && typeof value === "object") {
50
+ for (const [key, nested] of Object.entries(value)) {
51
+ const nextId = fieldId ? `${fieldId}.${key}` : key;
52
+ walkElementTemplateFields(nested, nextId, results, t);
53
+ }
54
+ }
55
+ }
56
+
57
+ export function collectElementScopeTemplateFields(
58
+ element: PanelElement,
59
+ t: TranslateFn = tForLocale("zh-CN")
60
+ ): ScopeTemplateField[] {
61
+ const results: ScopeTemplateField[] = [];
62
+ walkElementTemplateFields(element, "", results, t);
63
+ return results;
64
+ }
65
+
66
+ export function matchScopeTemplateFieldId(
67
+ template: string,
68
+ fields: ScopeTemplateField[]
69
+ ): string | undefined {
70
+ const exact = fields.find((field) => field.template === template);
71
+ if (exact) return exact.fieldId;
72
+
73
+ if (!hasScopeTemplate(template)) return undefined;
74
+
75
+ const partial = fields.filter(
76
+ (field) =>
77
+ field.template.includes(template) || template.includes(field.template)
78
+ );
79
+ if (partial.length === 1) return partial[0].fieldId;
80
+ return undefined;
81
+ }
82
+
83
+ export function resolveScopeTemplatePreview(
84
+ template: string,
85
+ scope: unknown,
86
+ fieldId?: string
87
+ ): unknown {
88
+ if (!hasScopeTemplate(template)) return template;
89
+ if (scope === undefined) return undefined;
90
+
91
+ const leafFieldId = fieldId?.split(".").pop();
92
+
93
+ if (
94
+ fieldId === "chart.labelsText" ||
95
+ leafFieldId === "labelsText"
96
+ ) {
97
+ return materializeChartLabelsText(template, scope);
98
+ }
99
+
100
+ if (
101
+ fieldId === "chart.valuesText" ||
102
+ leafFieldId === "valuesText"
103
+ ) {
104
+ return materializeChartValuesText(template, scope);
105
+ }
106
+
107
+ if (SCOPE_SPREAD_TEMPLATE_RE.test(template)) {
108
+ return materializeChartLabelsText(template, scope);
109
+ }
110
+
111
+ const evaluated = evaluateScopeTemplate(template, scope);
112
+ try {
113
+ return JSON.parse(evaluated);
114
+ } catch {
115
+ return evaluated;
116
+ }
117
+ }
118
+
119
+ export function formatScopeTemplatePreview(value: unknown): string {
120
+ if (value === undefined) return "undefined";
121
+ if (value === null) return "null";
122
+ if (typeof value === "string") return value;
123
+ try {
124
+ return JSON.stringify(value, null, 2);
125
+ } catch {
126
+ return String(value);
127
+ }
128
+ }
@@ -0,0 +1,252 @@
1
+ /** 数组展开模版:[...{scope?.a?.b}] 从 scope 中定位数组并提取每项字段,输出逗号分隔值 */
2
+
3
+ import type { TranslateFn } from "@arronqzy/i18n";
4
+ import { tForLocale } from "@arronqzy/i18n";
5
+
6
+ export const SCOPE_SPREAD_TEMPLATE_RE = /\[\.\.\.\{([^}]+)\}\]/g;
7
+
8
+ export const SCOPE_SPREAD_TEMPLATE_DEMO = {
9
+ scope: { a: [{ b: 1 }, { b: 2 }] },
10
+ template: "[...{scope?.a?.b}]",
11
+ output: "1,2",
12
+ } as const;
13
+
14
+ export function getScopeSpreadDeepTemplateDemo(t: TranslateFn = tForLocale("zh-CN")) {
15
+ const inProgress = t("panel.scope.demoInProgress");
16
+ const done = t("panel.scope.demoDone");
17
+ return {
18
+ scope: {
19
+ data: {
20
+ data: [{ statusName: inProgress }, { statusName: done }],
21
+ },
22
+ },
23
+ template: "[...{scope?.data?.data?.statusName}]",
24
+ output: t("panel.scope.demoJoined"),
25
+ } as const;
26
+ }
27
+
28
+ /** @deprecated Prefer getScopeSpreadDeepTemplateDemo(t) */
29
+ export const SCOPE_SPREAD_DEEP_TEMPLATE_DEMO = getScopeSpreadDeepTemplateDemo();
30
+
31
+ export type ScopeSpreadEvalIssue =
32
+ | "invalid-expression"
33
+ | "path-missing"
34
+ | "not-array";
35
+
36
+ export type ScopeSpreadEvalResult =
37
+ | { ok: true; value: string }
38
+ | { ok: false; issue: ScopeSpreadEvalIssue; arrayPath?: string };
39
+
40
+ export type SpreadScopePathResolution =
41
+ | {
42
+ ok: true;
43
+ arrayPathSegments: string[];
44
+ itemPathSegments: string[];
45
+ arrayValue: unknown[];
46
+ }
47
+ | { ok: false; issue: ScopeSpreadEvalIssue; arrayPath?: string };
48
+
49
+ /** 解析 scope?.a?.b 形式的路径段(不含 scope 前缀) */
50
+ export function parseScopePathSegments(expression: string): string[] | null {
51
+ const trimmed = expression.trim();
52
+ if (!trimmed.startsWith("scope")) return null;
53
+
54
+ let body = trimmed.slice("scope".length);
55
+ if (body.startsWith("?.")) body = body.slice(2);
56
+ else if (body.startsWith(".")) body = body.slice(1);
57
+ else if (body.length > 0) return null;
58
+
59
+ if (!body) return [];
60
+ return body.split("?.").filter(Boolean);
61
+ }
62
+
63
+ export function formatScopePathSegments(segments: string[]): string {
64
+ return segments.join(".");
65
+ }
66
+
67
+ export function getValueAtScopePath(scope: unknown, path: string[]): unknown {
68
+ let current: unknown = scope;
69
+ for (const part of path) {
70
+ if (current === null || current === undefined) return undefined;
71
+ if (typeof current !== "object") return undefined;
72
+ current = (current as Record<string, unknown>)[part];
73
+ }
74
+ return current;
75
+ }
76
+
77
+ /**
78
+ * 从完整路径中定位数组段与项内字段段。
79
+ * 例如 scope?.data?.data?.statusName → 数组在 data.data,项字段为 statusName。
80
+ */
81
+ export function resolveSpreadScopePath(
82
+ expression: string,
83
+ scope: unknown
84
+ ): SpreadScopePathResolution {
85
+ const segments = parseScopePathSegments(expression);
86
+ if (segments === null || segments.length === 0) {
87
+ return { ok: false, issue: "invalid-expression" };
88
+ }
89
+
90
+ if (segments.length === 1) {
91
+ const arrayPathSegments = segments;
92
+ const arrayValue = getValueAtScopePath(scope, arrayPathSegments);
93
+ const arrayPath = formatScopePathSegments(arrayPathSegments);
94
+ if (arrayValue === undefined) {
95
+ return { ok: false, issue: "path-missing", arrayPath };
96
+ }
97
+ if (!Array.isArray(arrayValue)) {
98
+ return { ok: false, issue: "not-array", arrayPath };
99
+ }
100
+ return {
101
+ ok: true,
102
+ arrayPathSegments,
103
+ itemPathSegments: [],
104
+ arrayValue,
105
+ };
106
+ }
107
+
108
+ for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
109
+ const arrayPathSegments = segments.slice(0, splitAt);
110
+ const itemPathSegments = segments.slice(splitAt);
111
+ const arrayValue = getValueAtScopePath(scope, arrayPathSegments);
112
+ if (arrayValue === undefined) continue;
113
+ if (Array.isArray(arrayValue)) {
114
+ return {
115
+ ok: true,
116
+ arrayPathSegments,
117
+ itemPathSegments,
118
+ arrayValue,
119
+ };
120
+ }
121
+ }
122
+
123
+ const rootValue = getValueAtScopePath(scope, [segments[0]!]);
124
+ if (rootValue === undefined) {
125
+ return {
126
+ ok: false,
127
+ issue: "path-missing",
128
+ arrayPath: segments[0],
129
+ };
130
+ }
131
+
132
+ for (let splitAt = 1; splitAt < segments.length; splitAt++) {
133
+ const arrayPathSegments = segments.slice(0, splitAt);
134
+ const arrayValue = getValueAtScopePath(scope, arrayPathSegments);
135
+ if (arrayValue === undefined) continue;
136
+ if (!Array.isArray(arrayValue)) {
137
+ return {
138
+ ok: false,
139
+ issue: "not-array",
140
+ arrayPath: formatScopePathSegments(arrayPathSegments),
141
+ };
142
+ }
143
+ }
144
+
145
+ return {
146
+ ok: false,
147
+ issue: "path-missing",
148
+ arrayPath: formatScopePathSegments(segments.slice(0, -1)),
149
+ };
150
+ }
151
+
152
+ function formatSpreadItemValue(value: unknown): string {
153
+ if (value === null || value === undefined) return "";
154
+ if (typeof value === "string") return value;
155
+ if (typeof value === "number" || typeof value === "boolean") {
156
+ return String(value);
157
+ }
158
+ try {
159
+ return JSON.stringify(value);
160
+ } catch {
161
+ return String(value);
162
+ }
163
+ }
164
+
165
+ export function evaluateSpreadScopeExpression(
166
+ expression: string,
167
+ scope: unknown
168
+ ): ScopeSpreadEvalResult {
169
+ const valuesResult = evaluateSpreadScopeExpressionValues(expression, scope);
170
+ if (!valuesResult.ok) {
171
+ return valuesResult;
172
+ }
173
+ return { ok: true, value: valuesResult.values.join(",") };
174
+ }
175
+
176
+ export function evaluateSpreadScopeExpressionValues(
177
+ expression: string,
178
+ scope: unknown
179
+ ): { ok: true; values: string[] } | { ok: false; issue: ScopeSpreadEvalIssue; arrayPath?: string } {
180
+ const resolved = resolveSpreadScopePath(expression, scope);
181
+ if (!resolved.ok) {
182
+ return {
183
+ ok: false,
184
+ issue: resolved.issue,
185
+ arrayPath: resolved.arrayPath,
186
+ };
187
+ }
188
+
189
+ const values = resolved.arrayValue
190
+ .map((item) =>
191
+ resolved.itemPathSegments.length === 0
192
+ ? item
193
+ : getValueAtScopePath(item, resolved.itemPathSegments)
194
+ )
195
+ .map(formatSpreadItemValue)
196
+ .filter((value) => value.length > 0);
197
+
198
+ return { ok: true, values };
199
+ }
200
+
201
+ export function isSpreadOnlyScopeTemplate(value: string): boolean {
202
+ return /^\[\.\.\.\{[^}]+\}\]$/.test(value.trim());
203
+ }
204
+
205
+ export function extractSpreadScopeExpression(template: string): string | null {
206
+ const match = /^\[\.\.\.\{([^}]+)\}\]$/.exec(template.trim());
207
+ return match ? match[1]!.trim() : null;
208
+ }
209
+
210
+ export function isSpreadTemplateBrace(value: string, braceIndex: number): boolean {
211
+ return value.slice(Math.max(0, braceIndex - 4), braceIndex) === "[...";
212
+ }
213
+
214
+ export function buildSpreadNotArrayWarningMessage(
215
+ fieldLabel: string,
216
+ template: string,
217
+ expression: string,
218
+ arrayPath: string,
219
+ t: TranslateFn = tForLocale("zh-CN")
220
+ ): string {
221
+ void expression;
222
+ const demoScope = JSON.stringify(SCOPE_SPREAD_TEMPLATE_DEMO.scope);
223
+ const deepDemo = getScopeSpreadDeepTemplateDemo(t);
224
+ return t("panel.scope.warnSpreadNotArray", {
225
+ fieldLabel,
226
+ template,
227
+ arrayPath,
228
+ demoScope,
229
+ demoTemplate: SCOPE_SPREAD_TEMPLATE_DEMO.template,
230
+ demoOutput: SCOPE_SPREAD_TEMPLATE_DEMO.output,
231
+ deepTemplate: deepDemo.template,
232
+ deepScope: JSON.stringify(deepDemo.scope),
233
+ deepOutput: deepDemo.output,
234
+ });
235
+ }
236
+
237
+ export function buildSpreadInvalidExpressionWarningMessage(
238
+ fieldLabel: string,
239
+ template: string,
240
+ t: TranslateFn = tForLocale("zh-CN")
241
+ ): string {
242
+ const demoScope = JSON.stringify(SCOPE_SPREAD_TEMPLATE_DEMO.scope);
243
+ const deepDemo = getScopeSpreadDeepTemplateDemo(t);
244
+ return t("panel.scope.warnSpreadInvalid", {
245
+ fieldLabel,
246
+ template,
247
+ demoTemplate: SCOPE_SPREAD_TEMPLATE_DEMO.template,
248
+ deepTemplate: deepDemo.template,
249
+ demoScope,
250
+ demoOutput: SCOPE_SPREAD_TEMPLATE_DEMO.output,
251
+ });
252
+ }