@arronqzy/react-view 1.0.3 → 1.0.5

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.
@@ -0,0 +1,229 @@
1
+ /** 数组展开模版:[...{scope?.a?.b}] 从 scope 中定位数组并提取每项字段,输出逗号分隔值 */
2
+
3
+ export const SCOPE_SPREAD_TEMPLATE_RE = /\[\.\.\.\{([^}]+)\}\]/g;
4
+
5
+ export const SCOPE_SPREAD_TEMPLATE_DEMO = {
6
+ scope: { a: [{ b: 1 }, { b: 2 }] },
7
+ template: "[...{scope?.a?.b}]",
8
+ output: "1,2",
9
+ } as const;
10
+
11
+ export const SCOPE_SPREAD_DEEP_TEMPLATE_DEMO = {
12
+ scope: {
13
+ data: {
14
+ data: [{ statusName: "进行中" }, { statusName: "已完成" }],
15
+ },
16
+ },
17
+ template: "[...{scope?.data?.data?.statusName}]",
18
+ output: "进行中,已完成",
19
+ } as const;
20
+
21
+ export type ScopeSpreadEvalIssue =
22
+ | "invalid-expression"
23
+ | "path-missing"
24
+ | "not-array";
25
+
26
+ export type ScopeSpreadEvalResult =
27
+ | { ok: true; value: string }
28
+ | { ok: false; issue: ScopeSpreadEvalIssue; arrayPath?: string };
29
+
30
+ export type SpreadScopePathResolution =
31
+ | {
32
+ ok: true;
33
+ arrayPathSegments: string[];
34
+ itemPathSegments: string[];
35
+ arrayValue: unknown[];
36
+ }
37
+ | { ok: false; issue: ScopeSpreadEvalIssue; arrayPath?: string };
38
+
39
+ /** 解析 scope?.a?.b 形式的路径段(不含 scope 前缀) */
40
+ export function parseScopePathSegments(expression: string): string[] | null {
41
+ const trimmed = expression.trim();
42
+ if (!trimmed.startsWith("scope")) return null;
43
+
44
+ let body = trimmed.slice("scope".length);
45
+ if (body.startsWith("?.")) body = body.slice(2);
46
+ else if (body.startsWith(".")) body = body.slice(1);
47
+ else if (body.length > 0) return null;
48
+
49
+ if (!body) return [];
50
+ return body.split("?.").filter(Boolean);
51
+ }
52
+
53
+ export function formatScopePathSegments(segments: string[]): string {
54
+ return segments.join(".");
55
+ }
56
+
57
+ export function getValueAtScopePath(scope: unknown, path: string[]): unknown {
58
+ let current: unknown = scope;
59
+ for (const part of path) {
60
+ if (current === null || current === undefined) return undefined;
61
+ if (typeof current !== "object") return undefined;
62
+ current = (current as Record<string, unknown>)[part];
63
+ }
64
+ return current;
65
+ }
66
+
67
+ /**
68
+ * 从完整路径中定位数组段与项内字段段。
69
+ * 例如 scope?.data?.data?.statusName → 数组在 data.data,项字段为 statusName。
70
+ */
71
+ export function resolveSpreadScopePath(
72
+ expression: string,
73
+ scope: unknown
74
+ ): SpreadScopePathResolution {
75
+ const segments = parseScopePathSegments(expression);
76
+ if (segments === null || segments.length === 0) {
77
+ return { ok: false, issue: "invalid-expression" };
78
+ }
79
+
80
+ if (segments.length === 1) {
81
+ const arrayPathSegments = segments;
82
+ const arrayValue = getValueAtScopePath(scope, arrayPathSegments);
83
+ const arrayPath = formatScopePathSegments(arrayPathSegments);
84
+ if (arrayValue === undefined) {
85
+ return { ok: false, issue: "path-missing", arrayPath };
86
+ }
87
+ if (!Array.isArray(arrayValue)) {
88
+ return { ok: false, issue: "not-array", arrayPath };
89
+ }
90
+ return {
91
+ ok: true,
92
+ arrayPathSegments,
93
+ itemPathSegments: [],
94
+ arrayValue,
95
+ };
96
+ }
97
+
98
+ for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
99
+ const arrayPathSegments = segments.slice(0, splitAt);
100
+ const itemPathSegments = segments.slice(splitAt);
101
+ const arrayValue = getValueAtScopePath(scope, arrayPathSegments);
102
+ if (arrayValue === undefined) continue;
103
+ if (Array.isArray(arrayValue)) {
104
+ return {
105
+ ok: true,
106
+ arrayPathSegments,
107
+ itemPathSegments,
108
+ arrayValue,
109
+ };
110
+ }
111
+ }
112
+
113
+ const rootValue = getValueAtScopePath(scope, [segments[0]!]);
114
+ if (rootValue === undefined) {
115
+ return {
116
+ ok: false,
117
+ issue: "path-missing",
118
+ arrayPath: segments[0],
119
+ };
120
+ }
121
+
122
+ for (let splitAt = 1; splitAt < segments.length; splitAt++) {
123
+ const arrayPathSegments = segments.slice(0, splitAt);
124
+ const arrayValue = getValueAtScopePath(scope, arrayPathSegments);
125
+ if (arrayValue === undefined) continue;
126
+ if (!Array.isArray(arrayValue)) {
127
+ return {
128
+ ok: false,
129
+ issue: "not-array",
130
+ arrayPath: formatScopePathSegments(arrayPathSegments),
131
+ };
132
+ }
133
+ }
134
+
135
+ return {
136
+ ok: false,
137
+ issue: "path-missing",
138
+ arrayPath: formatScopePathSegments(segments.slice(0, -1)),
139
+ };
140
+ }
141
+
142
+ function formatSpreadItemValue(value: unknown): string {
143
+ if (value === null || value === undefined) return "";
144
+ if (typeof value === "string") return value;
145
+ if (typeof value === "number" || typeof value === "boolean") {
146
+ return String(value);
147
+ }
148
+ try {
149
+ return JSON.stringify(value);
150
+ } catch {
151
+ return String(value);
152
+ }
153
+ }
154
+
155
+ export function evaluateSpreadScopeExpression(
156
+ expression: string,
157
+ scope: unknown
158
+ ): ScopeSpreadEvalResult {
159
+ const valuesResult = evaluateSpreadScopeExpressionValues(expression, scope);
160
+ if (!valuesResult.ok) {
161
+ return valuesResult;
162
+ }
163
+ return { ok: true, value: valuesResult.values.join(",") };
164
+ }
165
+
166
+ export function evaluateSpreadScopeExpressionValues(
167
+ expression: string,
168
+ scope: unknown
169
+ ): { ok: true; values: string[] } | { ok: false; issue: ScopeSpreadEvalIssue; arrayPath?: string } {
170
+ const resolved = resolveSpreadScopePath(expression, scope);
171
+ if (!resolved.ok) {
172
+ return {
173
+ ok: false,
174
+ issue: resolved.issue,
175
+ arrayPath: resolved.arrayPath,
176
+ };
177
+ }
178
+
179
+ const values = resolved.arrayValue
180
+ .map((item) =>
181
+ resolved.itemPathSegments.length === 0
182
+ ? item
183
+ : getValueAtScopePath(item, resolved.itemPathSegments)
184
+ )
185
+ .map(formatSpreadItemValue)
186
+ .filter((value) => value.length > 0);
187
+
188
+ return { ok: true, values };
189
+ }
190
+
191
+ export function isSpreadOnlyScopeTemplate(value: string): boolean {
192
+ return /^\[\.\.\.\{[^}]+\}\]$/.test(value.trim());
193
+ }
194
+
195
+ export function extractSpreadScopeExpression(template: string): string | null {
196
+ const match = /^\[\.\.\.\{([^}]+)\}\]$/.exec(template.trim());
197
+ return match ? match[1]!.trim() : null;
198
+ }
199
+
200
+ export function isSpreadTemplateBrace(value: string, braceIndex: number): boolean {
201
+ return value.slice(Math.max(0, braceIndex - 4), braceIndex) === "[...";
202
+ }
203
+
204
+ export function buildSpreadNotArrayWarningMessage(
205
+ fieldLabel: string,
206
+ template: string,
207
+ expression: string,
208
+ arrayPath: string
209
+ ): string {
210
+ const demoScope = JSON.stringify(SCOPE_SPREAD_TEMPLATE_DEMO.scope);
211
+ const deepDemo = SCOPE_SPREAD_DEEP_TEMPLATE_DEMO;
212
+ return (
213
+ `「${fieldLabel}」中的 ${template} 模版仅可做数组值的映射,当前 scope.${arrayPath} 不是数组,数据不符合规则。` +
214
+ `参考示例:scope 为 ${demoScope} 时使用 ${SCOPE_SPREAD_TEMPLATE_DEMO.template} 输出 ${SCOPE_SPREAD_TEMPLATE_DEMO.output};` +
215
+ `深层路径可用 ${deepDemo.template}(scope 为 ${JSON.stringify(deepDemo.scope)} 时输出 ${deepDemo.output})。`
216
+ );
217
+ }
218
+
219
+ export function buildSpreadInvalidExpressionWarningMessage(
220
+ fieldLabel: string,
221
+ template: string
222
+ ): string {
223
+ const demoScope = JSON.stringify(SCOPE_SPREAD_TEMPLATE_DEMO.scope);
224
+ const deepDemo = SCOPE_SPREAD_DEEP_TEMPLATE_DEMO;
225
+ return (
226
+ `「${fieldLabel}」中的 ${template} 写法无效,请使用 ${SCOPE_SPREAD_TEMPLATE_DEMO.template} 或 ${deepDemo.template} 形式。` +
227
+ `参考示例:scope 为 ${demoScope} 时输出 ${SCOPE_SPREAD_TEMPLATE_DEMO.output}。`
228
+ );
229
+ }
@@ -1,6 +1,20 @@
1
1
  import type { PanelElement } from "../types";
2
2
  import { hasScopeTemplate } from "./scope-template";
3
3
  import { resolveScopeFieldLabel } from "./scope-field-labels";
4
+ import {
5
+ SCOPE_SPREAD_TEMPLATE_RE,
6
+ buildSpreadInvalidExpressionWarningMessage,
7
+ buildSpreadNotArrayWarningMessage,
8
+ formatScopePathSegments,
9
+ isSpreadTemplateBrace,
10
+ parseScopePathSegments,
11
+ resolveSpreadScopePath,
12
+ } from "./scope-template-spread";
13
+
14
+ export type ScopeTemplateWarningKind =
15
+ | "missing-path"
16
+ | "spread-not-array"
17
+ | "spread-invalid-expression";
4
18
 
5
19
  export type ScopeTemplateWarning = {
6
20
  fieldId: string;
@@ -9,6 +23,7 @@ export type ScopeTemplateWarning = {
9
23
  expression: string;
10
24
  missingPath: string;
11
25
  message: string;
26
+ kind: ScopeTemplateWarningKind;
12
27
  };
13
28
 
14
29
  const SCOPE_PATH_RE = /scope(?:\?\.|\.)(\w+(?:\?\.\w+)*)/g;
@@ -41,6 +56,95 @@ function pathExistsInScope(scope: unknown, path: string): boolean {
41
56
  return true;
42
57
  }
43
58
 
59
+ function analyzeSpreadTemplateValue(
60
+ fieldId: string,
61
+ value: string,
62
+ scope: unknown
63
+ ): ScopeTemplateWarning[] {
64
+ const warnings: ScopeTemplateWarning[] = [];
65
+ const fieldLabel = resolveScopeFieldLabel(fieldId);
66
+ const spreadRe = new RegExp(SCOPE_SPREAD_TEMPLATE_RE.source, "g");
67
+ let match: RegExpExecArray | null;
68
+
69
+ while ((match = spreadRe.exec(value)) !== null) {
70
+ const spreadTemplate = match[0];
71
+ const expression = match[1].trim();
72
+ const segments = parseScopePathSegments(expression);
73
+
74
+ if (segments === null || segments.length === 0) {
75
+ warnings.push({
76
+ fieldId,
77
+ fieldLabel,
78
+ template: value,
79
+ expression,
80
+ missingPath: "",
81
+ kind: "spread-invalid-expression",
82
+ message: buildSpreadInvalidExpressionWarningMessage(
83
+ fieldLabel,
84
+ spreadTemplate
85
+ ),
86
+ });
87
+ continue;
88
+ }
89
+
90
+ const resolved = resolveSpreadScopePath(expression, scope);
91
+ if (resolved.ok) continue;
92
+
93
+ if (resolved.issue === "invalid-expression") {
94
+ warnings.push({
95
+ fieldId,
96
+ fieldLabel,
97
+ template: value,
98
+ expression,
99
+ missingPath: "",
100
+ kind: "spread-invalid-expression",
101
+ message: buildSpreadInvalidExpressionWarningMessage(
102
+ fieldLabel,
103
+ spreadTemplate
104
+ ),
105
+ });
106
+ continue;
107
+ }
108
+
109
+ const arrayPath =
110
+ resolved.arrayPath ?? formatScopePathSegments(segments.slice(0, -1));
111
+
112
+ if (resolved.issue === "path-missing") {
113
+ if (!pathExistsInScope(scope, arrayPath)) {
114
+ warnings.push({
115
+ fieldId,
116
+ fieldLabel,
117
+ template: value,
118
+ expression,
119
+ missingPath: arrayPath,
120
+ kind: "missing-path",
121
+ message: `「${fieldLabel}」引用了 scope.${arrayPath},但当前 scope 数据中并没有「${arrayPath}」字段,建议检查一下。`,
122
+ });
123
+ }
124
+ continue;
125
+ }
126
+
127
+ if (resolved.issue === "not-array") {
128
+ warnings.push({
129
+ fieldId,
130
+ fieldLabel,
131
+ template: value,
132
+ expression,
133
+ missingPath: arrayPath,
134
+ kind: "spread-not-array",
135
+ message: buildSpreadNotArrayWarningMessage(
136
+ fieldLabel,
137
+ spreadTemplate,
138
+ expression,
139
+ arrayPath
140
+ ),
141
+ });
142
+ }
143
+ }
144
+
145
+ return warnings;
146
+ }
147
+
44
148
  function analyzeTemplateValue(
45
149
  fieldId: string,
46
150
  value: string,
@@ -48,12 +152,15 @@ function analyzeTemplateValue(
48
152
  ): ScopeTemplateWarning[] {
49
153
  if (!hasScopeTemplate(value)) return [];
50
154
 
51
- const warnings: ScopeTemplateWarning[] = [];
155
+ const warnings: ScopeTemplateWarning[] = [
156
+ ...analyzeSpreadTemplateValue(fieldId, value, scope),
157
+ ];
52
158
  const fieldLabel = resolveScopeFieldLabel(fieldId);
53
159
  const expressionRe = /\{([^}]+)\}/g;
54
160
  let match: RegExpExecArray | null;
55
161
 
56
162
  while ((match = expressionRe.exec(value)) !== null) {
163
+ if (isSpreadTemplateBrace(value, match.index)) continue;
57
164
  const expression = match[1].trim();
58
165
  const paths = extractScopePropertyPaths(expression);
59
166
  for (const path of paths) {
@@ -64,6 +171,7 @@ function analyzeTemplateValue(
64
171
  template: value,
65
172
  expression,
66
173
  missingPath: path,
174
+ kind: "missing-path",
67
175
  message: `「${fieldLabel}」引用了 scope.${path},但当前 scope 数据中并没有「${path}」字段,建议检查一下。`,
68
176
  });
69
177
  }
@@ -115,7 +223,7 @@ export function collectElementScopeWarnings(
115
223
 
116
224
  const seen = new Set<string>();
117
225
  return warnings.filter((warning) => {
118
- const key = `${warning.fieldId}|${warning.missingPath}|${warning.expression}`;
226
+ const key = `${warning.fieldId}|${warning.kind}|${warning.missingPath}|${warning.expression}`;
119
227
  if (seen.has(key)) return false;
120
228
  seen.add(key);
121
229
  return true;
@@ -1,6 +1,11 @@
1
1
  import type { PanelElement } from "../types";
2
+ import { materializeChartLabelsFromScope } from "./scope-template-chart";
3
+ import {
4
+ SCOPE_SPREAD_TEMPLATE_RE,
5
+ evaluateSpreadScopeExpression,
6
+ } from "./scope-template-spread";
2
7
 
3
- const SCOPE_TEMPLATE_RE = /\{[^}]+\}/;
8
+ const SCOPE_TEMPLATE_RE = /\{[^}]+\}|\[\.\.\.\{[^}]+\}\]/;
4
9
 
5
10
  export function hasScopeTemplate(value: string): boolean {
6
11
  return SCOPE_TEMPLATE_RE.test(value);
@@ -25,7 +30,16 @@ export function evaluateScopeTemplate(
25
30
  scope: unknown
26
31
  ): string {
27
32
  if (!hasScopeTemplate(template)) return template;
28
- return template.replace(/\{([^}]+)\}/g, (_, rawExpr: string) => {
33
+
34
+ const withSpread = template.replace(
35
+ SCOPE_SPREAD_TEMPLATE_RE,
36
+ (_, rawExpr: string) => {
37
+ const result = evaluateSpreadScopeExpression(rawExpr, scope);
38
+ return result.ok ? result.value : "";
39
+ }
40
+ );
41
+
42
+ return withSpread.replace(/\{([^}]+)\}/g, (_, rawExpr: string) => {
29
43
  const value = evaluateScopeExpression(rawExpr, scope);
30
44
  if (value === null || value === undefined) return "";
31
45
  if (typeof value === "string") return value;
@@ -62,7 +76,15 @@ export function resolvePanelElementScope(
62
76
  scope: unknown | undefined
63
77
  ): PanelElement {
64
78
  if (scope === undefined) return element;
65
- return resolveScopedValue(structuredClone(element), scope);
79
+ const resolved = resolveScopedValue(structuredClone(element), scope);
80
+ if (resolved.chart) {
81
+ const labels = materializeChartLabelsFromScope(element.chart, scope);
82
+ if (labels !== undefined) {
83
+ resolved.chart.labels = labels;
84
+ delete resolved.chart.labelsText;
85
+ }
86
+ }
87
+ return resolved;
66
88
  }
67
89
 
68
90
  export function formatViewElementScope(scope: unknown): string {
package/src/tailwind.css CHANGED
@@ -29,30 +29,43 @@
29
29
  /* Moveable:仅角点控制柄固定屏幕像素;勿作用于 moveable-direction 边线 */
30
30
  .rv-moveable-layer {
31
31
  --moveable-color: #1990ff;
32
+ z-index: 9999 !important;
32
33
  }
33
34
 
34
- .rv-moveable-layer .moveable-control {
35
- width: 14px !important;
36
- height: 14px !important;
37
- margin-top: -7px !important;
38
- margin-left: -7px !important;
39
- border-radius: 50% !important;
40
- box-sizing: border-box !important;
41
- border: 2px solid hsl(var(--background)) !important;
42
- background: #1990ff !important;
35
+ .rv-moveable-layer.moveable-group,
36
+ .rv-moveable-layer .moveable-group,
37
+ .rv-moveable-layer.moveable-control-box,
38
+ .rv-moveable-layer .moveable-control-box {
39
+ z-index: 1 !important;
40
+ }
41
+
42
+ .rv-moveable-layer .moveable-line {
43
+ --moveable-color: #1990ff;
43
44
  background: var(--moveable-color) !important;
44
45
  }
45
46
 
47
+ /* 组选/单边控制线:厚度由 moveable zoom 补偿,保持屏幕 1px */
46
48
  .rv-moveable-layer .moveable-line.moveable-direction {
47
- width: 1px !important;
48
- height: 1px !important;
49
- margin: 0 !important;
49
+ background: var(--moveable-color) !important;
50
50
  }
51
51
 
52
- .rv-moveable-layer .moveable-line {
53
- --moveable-color: #1990ff;
52
+ .rv-moveable-layer .moveable-group .moveable-control-box,
53
+ .rv-moveable-layer.moveable-group .moveable-control-box {
54
+ z-index: 2 !important;
54
55
  }
55
56
 
56
57
  .rv-moveable-layer .moveable-around-control {
57
58
  background: transparent !important;
58
59
  }
60
+
61
+ .rv-moveable-layer .moveable-control {
62
+ width: 14px !important;
63
+ height: 14px !important;
64
+ margin-top: -7px !important;
65
+ margin-left: -7px !important;
66
+ border-radius: 50% !important;
67
+ box-sizing: border-box !important;
68
+ border: 2px solid hsl(var(--background)) !important;
69
+ background: #1990ff !important;
70
+ background: var(--moveable-color) !important;
71
+ }