@xbeeant/form-engine-designer 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 (117) hide show
  1. package/README.md +69 -0
  2. package/dist/es/Canvas.js +636 -0
  3. package/dist/es/Canvas.js.map +1 -0
  4. package/dist/es/Designer.js +162 -0
  5. package/dist/es/Designer.js.map +1 -0
  6. package/dist/es/DesignerContext.js +97 -0
  7. package/dist/es/DesignerContext.js.map +1 -0
  8. package/dist/es/Palette.js +148 -0
  9. package/dist/es/Palette.js.map +1 -0
  10. package/dist/es/PropertyPanel.js +294 -0
  11. package/dist/es/PropertyPanel.js.map +1 -0
  12. package/dist/es/ResizablePanel.js +103 -0
  13. package/dist/es/ResizablePanel.js.map +1 -0
  14. package/dist/es/catalog.js +514 -0
  15. package/dist/es/catalog.js.map +1 -0
  16. package/dist/es/form-engine-designer.css +1 -0
  17. package/dist/es/index.js +20 -0
  18. package/dist/es/index.js.map +1 -0
  19. package/dist/es/property/basic-property.js +115 -0
  20. package/dist/es/property/basic-property.js.map +1 -0
  21. package/dist/es/react/src/components/FormController.js +154 -0
  22. package/dist/es/react/src/components/FormController.js.map +1 -0
  23. package/dist/es/react/src/components/NexusField.js +99 -0
  24. package/dist/es/react/src/components/NexusField.js.map +1 -0
  25. package/dist/es/react/src/components/NexusForm.js +121 -0
  26. package/dist/es/react/src/components/NexusForm.js.map +1 -0
  27. package/dist/es/react/src/components/NexusFormProvider.js +19 -0
  28. package/dist/es/react/src/components/NexusFormProvider.js.map +1 -0
  29. package/dist/es/react/src/components/NexusLayout.js +49 -0
  30. package/dist/es/react/src/components/NexusLayout.js.map +1 -0
  31. package/dist/es/react/src/components/NexusObject.js +65 -0
  32. package/dist/es/react/src/components/NexusObject.js.map +1 -0
  33. package/dist/es/react/src/contexts/FieldInheritContext.js +6 -0
  34. package/dist/es/react/src/contexts/FieldInheritContext.js.map +1 -0
  35. package/dist/es/react/src/contexts/GridContext.js +6 -0
  36. package/dist/es/react/src/contexts/GridContext.js.map +1 -0
  37. package/dist/es/react/src/contexts/LayoutConfigContext.js +6 -0
  38. package/dist/es/react/src/contexts/LayoutConfigContext.js.map +1 -0
  39. package/dist/es/react/src/contexts/NexusContext.js +13 -0
  40. package/dist/es/react/src/contexts/NexusContext.js.map +1 -0
  41. package/dist/es/react/src/hooks/useForm.js +10 -0
  42. package/dist/es/react/src/hooks/useForm.js.map +1 -0
  43. package/dist/es/react/src/utils/renderTreeNode.js +18 -0
  44. package/dist/es/react/src/utils/renderTreeNode.js.map +1 -0
  45. package/dist/es/react/src/utils/resolveColSpan.js +10 -0
  46. package/dist/es/react/src/utils/resolveColSpan.js.map +1 -0
  47. package/dist/es/schemaUtils.js +286 -0
  48. package/dist/es/schemaUtils.js.map +1 -0
  49. package/dist/es/schemaUtils.test.js +188 -0
  50. package/dist/es/schemaUtils.test.js.map +1 -0
  51. package/dist/es/types.js +2 -0
  52. package/dist/es/types.js.map +1 -0
  53. package/dist/es/vite-env.d.js +2 -0
  54. package/dist/es/vite-env.d.js.map +1 -0
  55. package/dist/es/widgets/BindEditor.js +67 -0
  56. package/dist/es/widgets/BindEditor.js.map +1 -0
  57. package/dist/es/widgets/CodeEditor.js +84 -0
  58. package/dist/es/widgets/CodeEditor.js.map +1 -0
  59. package/dist/es/widgets/DefaultEditor.js +55 -0
  60. package/dist/es/widgets/DefaultEditor.js.map +1 -0
  61. package/dist/es/widgets/DependenciesEditor.js +36 -0
  62. package/dist/es/widgets/DependenciesEditor.js.map +1 -0
  63. package/dist/es/widgets/EnumEditor.js +49 -0
  64. package/dist/es/widgets/EnumEditor.js.map +1 -0
  65. package/dist/es/widgets/ExpressionBuilder.js +367 -0
  66. package/dist/es/widgets/ExpressionBuilder.js.map +1 -0
  67. package/dist/es/widgets/ExpressionSwitch.js +47 -0
  68. package/dist/es/widgets/ExpressionSwitch.js.map +1 -0
  69. package/dist/es/widgets/OptionsEditor.js +102 -0
  70. package/dist/es/widgets/OptionsEditor.js.map +1 -0
  71. package/dist/es/widgets/ReactionsEditor.js +401 -0
  72. package/dist/es/widgets/ReactionsEditor.js.map +1 -0
  73. package/dist/es/widgets/RemoteDataEditor.js +314 -0
  74. package/dist/es/widgets/RemoteDataEditor.js.map +1 -0
  75. package/dist/es/widgets/ValidateEditor.js +118 -0
  76. package/dist/es/widgets/ValidateEditor.js.map +1 -0
  77. package/dist/es/widgets/expressionModel.js +340 -0
  78. package/dist/es/widgets/expressionModel.js.map +1 -0
  79. package/dist/es/widgets/expressionModel.test.js +193 -0
  80. package/dist/es/widgets/expressionModel.test.js.map +1 -0
  81. package/dist/es/widgets/index.js +57 -0
  82. package/dist/es/widgets/index.js.map +1 -0
  83. package/dist/es/widgets/reactionsModel.js +107 -0
  84. package/dist/es/widgets/reactionsModel.js.map +1 -0
  85. package/dist/es/widgets/useFormDataFields.js +16 -0
  86. package/dist/es/widgets/useFormDataFields.js.map +1 -0
  87. package/package.json +62 -0
  88. package/src/Canvas.tsx +1111 -0
  89. package/src/Designer.css +206 -0
  90. package/src/Designer.tsx +254 -0
  91. package/src/DesignerContext.tsx +192 -0
  92. package/src/Palette.tsx +261 -0
  93. package/src/PropertyPanel.tsx +486 -0
  94. package/src/ResizablePanel.tsx +210 -0
  95. package/src/catalog.ts +526 -0
  96. package/src/index.ts +25 -0
  97. package/src/property/basic-property.ts +126 -0
  98. package/src/schemaUtils.test.ts +261 -0
  99. package/src/schemaUtils.ts +627 -0
  100. package/src/types.ts +38 -0
  101. package/src/vite-env.d.ts +1 -0
  102. package/src/widgets/BindEditor.tsx +105 -0
  103. package/src/widgets/CodeEditor.tsx +114 -0
  104. package/src/widgets/DefaultEditor.tsx +73 -0
  105. package/src/widgets/DependenciesEditor.tsx +56 -0
  106. package/src/widgets/EnumEditor.tsx +66 -0
  107. package/src/widgets/ExpressionBuilder.tsx +509 -0
  108. package/src/widgets/ExpressionSwitch.tsx +62 -0
  109. package/src/widgets/OptionsEditor.tsx +141 -0
  110. package/src/widgets/ReactionsEditor.tsx +532 -0
  111. package/src/widgets/RemoteDataEditor.tsx +382 -0
  112. package/src/widgets/ValidateEditor.tsx +162 -0
  113. package/src/widgets/expressionModel.test.ts +262 -0
  114. package/src/widgets/expressionModel.ts +573 -0
  115. package/src/widgets/index.ts +45 -0
  116. package/src/widgets/reactionsModel.ts +190 -0
  117. package/src/widgets/useFormDataFields.ts +21 -0
@@ -0,0 +1,509 @@
1
+ // ============================================================================
2
+ // ExpressionBuilder — 可视化表达式构建器
3
+ // 通过「变量 + 操作符 + 值」的条件行交互生成 `{{ ... }}` 布尔表达式,
4
+ // 避免手写表达式。保留「高级表达式」原始编辑回退(解析失败时自动进入)。
5
+ //
6
+ // 状态策略:本地工作副本(conditions/raw)为唯一编辑源,每次编辑即提交;
7
+ // 用 lastEmittedRef 跳过「自身 onChange → 引擎值回传」造成的受控重置,
8
+ // 外部 value 变化(切换节点等)才重建工作副本。
9
+ // ============================================================================
10
+
11
+ import { Button, Input, InputNumber, Select } from 'antd';
12
+ import { useEffect, useMemo, useRef, useState } from 'react';
13
+ import {
14
+ buildExpression,
15
+ type ConditionOperator,
16
+ createCondition,
17
+ type ExprCondition,
18
+ NO_RIGHT_OPERATORS,
19
+ OPERATOR_OPTIONS,
20
+ parseExpression,
21
+ parseVariable,
22
+ RIGHT_MODE_OPTIONS,
23
+ type RightMode,
24
+ stripBraces,
25
+ variableToString,
26
+ } from './expressionModel';
27
+
28
+ // ────────────────────────────────────────────────────────────────────────────
29
+ // VariableSelect — 变量选择器(表单字段 + 快捷变量)
30
+ // ────────────────────────────────────────────────────────────────────────────
31
+
32
+ interface VariableSelectProps {
33
+ value: string;
34
+ onChange: (text: string) => void;
35
+ fields: string[];
36
+ /** 可选字段标题列表(显示 title,值为路径 key);缺省时退化为 path 即 label */
37
+ fieldOptions?: Array<{ value: string; label: string }>;
38
+ depsCount: number;
39
+ showSelf: boolean;
40
+ placeholder?: string;
41
+ }
42
+
43
+ function VariableSelect({
44
+ value,
45
+ onChange,
46
+ fields,
47
+ fieldOptions,
48
+ depsCount,
49
+ showSelf,
50
+ placeholder,
51
+ }: VariableSelectProps) {
52
+ const groups = useMemo(() => {
53
+ const base =
54
+ fieldOptions && fieldOptions.length > 0
55
+ ? fieldOptions
56
+ : fields.map((f) => ({ value: f, label: f }));
57
+ const fieldOptions2 = base.map((o) => ({
58
+ value: `formData.${o.value}`,
59
+ label: o.label,
60
+ }));
61
+ const special: Array<{ value: string; label: string }> = [];
62
+ if (showSelf) {
63
+ special.push({
64
+ value: '$self.value',
65
+ label: '当前字段值($self.value)',
66
+ });
67
+ }
68
+ if (depsCount > 0) {
69
+ for (let i = 0; i < depsCount; i++) {
70
+ special.push({
71
+ value: `$deps[${i}]`,
72
+ label: `依赖 ${i + 1}($deps[${i}])`,
73
+ });
74
+ }
75
+ }
76
+ special.push({ value: '$index', label: '列表下标($index)' });
77
+ special.push({ value: 'rootValue', label: '根值(rootValue)' });
78
+ return [
79
+ ...(fieldOptions2.length > 0
80
+ ? [{ label: '表单字段', options: fieldOptions2 }]
81
+ : []),
82
+ ...(special.length > 0 ? [{ label: '快捷变量', options: special }] : []),
83
+ ];
84
+ }, [fields, fieldOptions, depsCount, showSelf]);
85
+
86
+ return (
87
+ <Select
88
+ size='small'
89
+ showSearch
90
+ allowClear
91
+ value={value || undefined}
92
+ placeholder={placeholder ?? '选择变量'}
93
+ onChange={(v) => onChange((v as string) || '')}
94
+ options={groups}
95
+ filterOption={(input, option) =>
96
+ ((option?.label as string) || '')
97
+ .toLowerCase()
98
+ .includes(input.toLowerCase())
99
+ }
100
+ style={{ width: '100%' }}
101
+ />
102
+ );
103
+ }
104
+
105
+ // ────────────────────────────────────────────────────────────────────────────
106
+ // 条件行编辑
107
+ // ────────────────────────────────────────────────────────────────────────────
108
+
109
+ function RightValueEditor({
110
+ mode,
111
+ value,
112
+ onChange,
113
+ fields,
114
+ fieldOptions,
115
+ depsCount,
116
+ showSelf,
117
+ }: {
118
+ mode: RightMode;
119
+ value: string;
120
+ onChange: (right: string) => void;
121
+ fields: string[];
122
+ fieldOptions?: Array<{ value: string; label: string }>;
123
+ depsCount: number;
124
+ showSelf: boolean;
125
+ }) {
126
+ if (mode === 'boolean') {
127
+ return (
128
+ <Select
129
+ size='small'
130
+ value={value === 'true' ? 'true' : 'false'}
131
+ onChange={(v) => onChange((v as string) || 'false')}
132
+ options={[
133
+ { value: 'true', label: 'true' },
134
+ { value: 'false', label: 'false' },
135
+ ]}
136
+ style={{ width: 72 }}
137
+ />
138
+ );
139
+ }
140
+ if (mode === 'variable') {
141
+ return (
142
+ <div style={{ flex: 1, minWidth: 100 }}>
143
+ <VariableSelect
144
+ value={value}
145
+ onChange={onChange}
146
+ fields={fields}
147
+ fieldOptions={fieldOptions}
148
+ depsCount={depsCount}
149
+ showSelf={showSelf}
150
+ placeholder='选择值变量'
151
+ />
152
+ </div>
153
+ );
154
+ }
155
+ if (mode === 'number') {
156
+ return (
157
+ <InputNumber
158
+ size='small'
159
+ value={value === '' ? undefined : Number(value)}
160
+ onChange={(v) => onChange(v == null ? '' : String(v))}
161
+ placeholder='数字'
162
+ style={{ flex: 1, minWidth: 80 }}
163
+ />
164
+ );
165
+ }
166
+ return (
167
+ <Input
168
+ size='small'
169
+ value={value}
170
+ onChange={(e) => onChange(e.target.value)}
171
+ placeholder='文本'
172
+ style={{ flex: 1, minWidth: 80 }}
173
+ />
174
+ );
175
+ }
176
+
177
+ function ConditionRow({
178
+ condition,
179
+ isFirst,
180
+ fields,
181
+ fieldOptions,
182
+ depsCount,
183
+ showSelf,
184
+ onChange,
185
+ onRemove,
186
+ }: {
187
+ condition: ExprCondition;
188
+ isFirst: boolean;
189
+ fields: string[];
190
+ fieldOptions?: Array<{ value: string; label: string }>;
191
+ depsCount: number;
192
+ showSelf: boolean;
193
+ onChange: (patch: Partial<ExprCondition>) => void;
194
+ onRemove: () => void;
195
+ }) {
196
+ const needRight = !NO_RIGHT_OPERATORS.has(condition.operator);
197
+
198
+ const handleOperator = (op: ConditionOperator) => {
199
+ onChange({
200
+ operator: op,
201
+ ...(NO_RIGHT_OPERATORS.has(op) ? { right: '' } : {}),
202
+ });
203
+ };
204
+
205
+ return (
206
+ <div
207
+ style={{
208
+ display: 'flex',
209
+ alignItems: 'center',
210
+ gap: 4,
211
+ marginBottom: 4,
212
+ flexWrap: 'wrap',
213
+ }}
214
+ >
215
+ {isFirst ? (
216
+ <span
217
+ style={{
218
+ width: 44,
219
+ fontSize: 11,
220
+ color: '#999',
221
+ flexShrink: 0,
222
+ textAlign: 'center',
223
+ }}
224
+ >
225
+
226
+ </span>
227
+ ) : (
228
+ <Select
229
+ size='small'
230
+ value={condition.logic}
231
+ onChange={(v) => onChange({ logic: v as 'and' | 'or' })}
232
+ options={[
233
+ { value: 'and', label: '且' },
234
+ { value: 'or', label: '或' },
235
+ ]}
236
+ style={{ width: 44, flexShrink: 0 }}
237
+ />
238
+ )}
239
+
240
+ <div style={{ flex: '1 1 110px', minWidth: 110 }}>
241
+ <VariableSelect
242
+ value={variableToString(condition.variable)}
243
+ onChange={(text) => onChange({ variable: parseVariable(text) })}
244
+ fields={fields}
245
+ fieldOptions={fieldOptions}
246
+ depsCount={depsCount}
247
+ showSelf={showSelf}
248
+ />
249
+ </div>
250
+
251
+ <Select
252
+ size='small'
253
+ value={condition.operator}
254
+ onChange={handleOperator}
255
+ options={OPERATOR_OPTIONS}
256
+ style={{ width: 92, flexShrink: 0 }}
257
+ />
258
+
259
+ {needRight && (
260
+ <>
261
+ <Select
262
+ size='small'
263
+ value={condition.rightMode}
264
+ onChange={(v) => onChange({ rightMode: v as RightMode })}
265
+ options={RIGHT_MODE_OPTIONS}
266
+ style={{ width: 56, flexShrink: 0 }}
267
+ />
268
+ <div style={{ flex: '1 1 80px', minWidth: 80 }}>
269
+ <RightValueEditor
270
+ mode={condition.rightMode}
271
+ value={condition.right}
272
+ onChange={(right) => onChange({ right })}
273
+ fields={fields}
274
+ fieldOptions={fieldOptions}
275
+ depsCount={depsCount}
276
+ showSelf={showSelf}
277
+ />
278
+ </div>
279
+ </>
280
+ )}
281
+
282
+ <Button
283
+ size='small'
284
+ type='text'
285
+ onClick={onRemove}
286
+ style={{ flexShrink: 0, color: '#bbb' }}
287
+ title='删除条件'
288
+ >
289
+ ×
290
+ </Button>
291
+ </div>
292
+ );
293
+ }
294
+
295
+ // ────────────────────────────────────────────────────────────────────────────
296
+ // ExpressionBuilder — 主组件
297
+ // ────────────────────────────────────────────────────────────────────────────
298
+
299
+ export interface ExpressionBuilderProps {
300
+ value?: string;
301
+ onChange: (next: string) => void;
302
+ /** 可选表单字段路径(formData.<path>) */
303
+ fields?: string[];
304
+ /** 可选字段标题列表(显示 title,值为路径 key);缺省时表单字段退化为 path 即 label */
305
+ fieldOptions?: Array<{ value: string; label: string }>;
306
+ /** 渲染 $deps[0..depsCount-1] 快捷变量 */
307
+ depsCount?: number;
308
+ showSelf?: boolean;
309
+ /** 条件全部移除时的提交值:template=保持表达式模式({{ }}),empty=清空('') */
310
+ emitEmpty?: 'template' | 'empty';
311
+ /** 是否显示「高级表达式」原始编辑入口 */
312
+ allowAdvanced?: boolean;
313
+ placeholder?: string;
314
+ }
315
+
316
+ function getInitMode(value: string | undefined): 'builder' | 'raw' {
317
+ if (typeof value !== 'string' || !value.trim()) {
318
+ return 'builder';
319
+ }
320
+ return parseExpression(value) === null ? 'raw' : 'builder';
321
+ }
322
+
323
+ function getInitConditions(value: string | undefined): ExprCondition[] {
324
+ if (typeof value !== 'string' || !value.trim()) {
325
+ return [];
326
+ }
327
+ const parsed = parseExpression(value);
328
+ return parsed === null ? [] : parsed;
329
+ }
330
+
331
+ function getInitRaw(value: string | undefined): string {
332
+ return typeof value === 'string' ? stripBraces(value) : '';
333
+ }
334
+
335
+ export function ExpressionBuilder({
336
+ value,
337
+ onChange,
338
+ fields = [],
339
+ fieldOptions,
340
+ depsCount = 0,
341
+ showSelf = true,
342
+ emitEmpty = 'template',
343
+ allowAdvanced = true,
344
+ placeholder = "{{ formData.xxx == 'yyy' }}",
345
+ }: ExpressionBuilderProps) {
346
+ const [mode, setMode] = useState<'builder' | 'raw'>(() => getInitMode(value));
347
+ const [conditions, setConditions] = useState<ExprCondition[]>(() =>
348
+ getInitConditions(value),
349
+ );
350
+ const [raw, setRaw] = useState<string>(() => getInitRaw(value));
351
+ const lastEmittedRef = useRef<string>(typeof value === 'string' ? value : '');
352
+
353
+ // 外部 value 变化(切换节点 / 属性表单重新初始化)时重建工作副本;
354
+ // 自身 onChange 引起的值回传不重置(lastEmittedRef 相等时跳过)
355
+ useEffect(() => {
356
+ if (value !== lastEmittedRef.current) {
357
+ lastEmittedRef.current = typeof value === 'string' ? value : '';
358
+ const parsed = typeof value === 'string' ? parseExpression(value) : [];
359
+ setConditions(parsed === null ? [] : parsed);
360
+ setMode(parsed === null ? 'raw' : 'builder');
361
+ setRaw(typeof value === 'string' ? stripBraces(value) : '');
362
+ }
363
+ }, [value]);
364
+
365
+ const emitBuilder = (next: ExprCondition[]) => {
366
+ setConditions(next);
367
+ const expr = buildExpression(next);
368
+ const out = expr === '{{ }}' && emitEmpty === 'empty' ? '' : expr;
369
+ lastEmittedRef.current = out;
370
+ onChange(out);
371
+ };
372
+
373
+ const emitRaw = (text: string) => {
374
+ setRaw(text);
375
+ lastEmittedRef.current = text;
376
+ onChange(text);
377
+ };
378
+
379
+ const switchToRaw = () => {
380
+ setRaw(typeof value === 'string' ? stripBraces(value) : '');
381
+ setMode('raw');
382
+ };
383
+
384
+ const switchToBuilder = () => {
385
+ const parsed = parseExpression(raw);
386
+ if (parsed === null) {
387
+ return;
388
+ }
389
+ setConditions(parsed);
390
+ setMode('builder');
391
+ };
392
+
393
+ const updateCondition = (id: string, patch: Partial<ExprCondition>) => {
394
+ emitBuilder(conditions.map((c) => (c.id === id ? { ...c, ...patch } : c)));
395
+ };
396
+
397
+ const removeCondition = (id: string) => {
398
+ emitBuilder(conditions.filter((c) => c.id !== id));
399
+ };
400
+
401
+ const addCondition = () => {
402
+ emitBuilder([...conditions, createCondition()]);
403
+ };
404
+
405
+ const rawParseFailed =
406
+ mode === 'raw' && raw.trim() !== '' && parseExpression(raw) === null;
407
+
408
+ return (
409
+ <div style={{ width: '100%' }}>
410
+ {mode === 'builder' ? (
411
+ <>
412
+ {conditions.length === 0 ? (
413
+ <div
414
+ style={{
415
+ fontSize: 11,
416
+ color: '#999',
417
+ background: '#fafafa',
418
+ border: '1px dashed #e0e0e0',
419
+ borderRadius: 6,
420
+ padding: '6px 8px',
421
+ marginBottom: 4,
422
+ }}
423
+ >
424
+ 点击下方「添加条件」组合 变量 / 操作符 / 值 生成表达式。
425
+ </div>
426
+ ) : (
427
+ conditions.map((c, i) => (
428
+ <ConditionRow
429
+ key={c.id}
430
+ condition={c}
431
+ isFirst={i === 0}
432
+ fields={fields}
433
+ fieldOptions={fieldOptions}
434
+ depsCount={depsCount}
435
+ showSelf={showSelf}
436
+ onChange={(patch) => updateCondition(c.id, patch)}
437
+ onRemove={() => removeCondition(c.id)}
438
+ />
439
+ ))
440
+ )}
441
+
442
+ <div
443
+ style={{
444
+ display: 'flex',
445
+ alignItems: 'center',
446
+ gap: 4,
447
+ marginTop: 2,
448
+ }}
449
+ >
450
+ <Button size='small' type='dashed' onClick={addCondition}>
451
+ + 添加条件
452
+ </Button>
453
+ {allowAdvanced && (
454
+ <Button
455
+ size='small'
456
+ type='link'
457
+ onClick={switchToRaw}
458
+ style={{ padding: '0 4px', height: 'auto', fontSize: 11 }}
459
+ >
460
+ 高级表达式
461
+ </Button>
462
+ )}
463
+ </div>
464
+ </>
465
+ ) : (
466
+ <>
467
+ <Input.TextArea
468
+ rows={2}
469
+ size='small'
470
+ value={raw}
471
+ onChange={(e) => emitRaw(e.target.value)}
472
+ placeholder={placeholder}
473
+ style={{ fontFamily: 'monospace', fontSize: 12 }}
474
+ />
475
+ <div
476
+ style={{
477
+ display: 'flex',
478
+ alignItems: 'center',
479
+ justifyContent: 'space-between',
480
+ marginTop: 2,
481
+ }}
482
+ >
483
+ <span
484
+ style={{
485
+ fontSize: 11,
486
+ color: rawParseFailed ? '#ff4d4f' : '#999',
487
+ }}
488
+ >
489
+ {rawParseFailed
490
+ ? '当前表达式无法结构化,可继续手动编辑或点击返回'
491
+ : '表达式需返回 boolean,变量仅限 $deps / $self / $form / $index / formData / rootValue'}
492
+ </span>
493
+ {allowAdvanced && (
494
+ <Button
495
+ size='small'
496
+ type='link'
497
+ onClick={switchToBuilder}
498
+ style={{ padding: '0 4px', height: 'auto', fontSize: 11 }}
499
+ disabled={rawParseFailed}
500
+ >
501
+ 返回可视化
502
+ </Button>
503
+ )}
504
+ </div>
505
+ </>
506
+ )}
507
+ </div>
508
+ );
509
+ }
@@ -0,0 +1,62 @@
1
+ // ============================================================================
2
+ // ExpressionSwitch — 编辑 ExpressionOr<boolean> 属性
3
+ // 支持静态布尔值(Switch)与表达式(可视化构建器 / 高级手写)之间的切换
4
+ // ============================================================================
5
+
6
+ import type { WidgetProps } from '@xbeeant/form-engine-ui';
7
+ import { Form, Switch } from 'antd';
8
+ import { ExpressionBuilder } from './ExpressionBuilder';
9
+ import { useFormDataFields } from './useFormDataFields';
10
+
11
+ export function ExpressionSwitch({
12
+ value,
13
+ onChange,
14
+ title,
15
+ displayType,
16
+ }: WidgetProps) {
17
+ // ExpressionOr<boolean>: 非空字符串=表达式模式,其他=静态模式
18
+ // 直接由 value 派生(受控),外部 value 变化(如切换选中节点)自动同步;
19
+ // 不再维护内部 isExpr state,避免与外部值脱节
20
+ const isExpr = typeof value === 'string' && value.length > 0;
21
+
22
+ const fields = useFormDataFields();
23
+
24
+ // 模式切换必须写回一个对应形态的值,否则状态只是视觉变化、无法持久化
25
+ const switchMode = (exprMode: boolean) => {
26
+ if (exprMode) {
27
+ // 切到表达式:保留已有表达式,静态布尔值则以空模板起步
28
+ onChange(typeof value === 'string' ? value : '{{ }}');
29
+ } else {
30
+ // 切到静态:reset 为布尔值
31
+ onChange(typeof value === 'boolean' ? value : false);
32
+ }
33
+ };
34
+
35
+ return (
36
+ <Form.Item
37
+ layout={displayType === 'row' ? 'horizontal' : 'vertical'}
38
+ label={title}
39
+ style={{ width: '100%' }}
40
+ >
41
+ <div className={`flex gap-2 ${isExpr ? 'flex-col' : 'flex-row'}`}>
42
+ {isExpr ? (
43
+ <ExpressionBuilder
44
+ value={value as string}
45
+ onChange={(s) => onChange(s)}
46
+ fields={fields}
47
+ />
48
+ ) : (
49
+ <Switch checked={!!value} onChange={(v) => onChange(v)} />
50
+ )}
51
+ <Switch
52
+ checked={isExpr}
53
+ checkedChildren='表达式'
54
+ unCheckedChildren='静态'
55
+ onChange={switchMode}
56
+ />
57
+ </div>
58
+ </Form.Item>
59
+ );
60
+ }
61
+
62
+ export const expressionSwitchWidget = ExpressionSwitch;
@@ -0,0 +1,141 @@
1
+ // ============================================================================
2
+ // OptionsEditor — 编辑选项列表(区分 value 与 label)
3
+ // 供 select / radio / checkboxes / multiSelect / checkbox / switch 等
4
+ // 选择类组件在属性面板中配置选项:
5
+ // 每行 = 选项值(value,存储进 formData)+ 选项文案(label,界面展示)
6
+ // value 支持字符串,'true'/'false' 自动转为布尔值(checkbox/switch 用)
7
+ // 值格式:Array<{ value: unknown; label: string }>
8
+ // ============================================================================
9
+
10
+ import type { WidgetProps } from '@xbeeant/form-engine-ui';
11
+ import { Button, Input, Space } from 'antd';
12
+ import { useMemo } from 'react';
13
+
14
+ interface OptionRow {
15
+ value: unknown;
16
+ label: string;
17
+ }
18
+
19
+ /** 归一化:兼容字符串数组(value=label)与 { value, label } 对象数组 */
20
+ const toRows = (value: unknown): OptionRow[] => {
21
+ if (!Array.isArray(value)) {
22
+ return [];
23
+ }
24
+ return value.map((item) => {
25
+ if (item && typeof item === 'object') {
26
+ const rec = item as Record<string, unknown>;
27
+ return {
28
+ value: rec.value,
29
+ label: rec.label == null ? '' : String(rec.label),
30
+ };
31
+ }
32
+ return { value: item, label: String(item) };
33
+ });
34
+ };
35
+
36
+ /** 输入回写:'true'/'false' → 布尔,其余保留字符串 */
37
+ const parseValue = (raw: string): unknown => {
38
+ const trimmed = raw.trim().toLowerCase();
39
+ if (trimmed === 'true') {
40
+ return true;
41
+ }
42
+ if (trimmed === 'false') {
43
+ return false;
44
+ }
45
+ return raw;
46
+ };
47
+
48
+ export function OptionsEditor({ value, onChange }: WidgetProps) {
49
+ const rows = useMemo(() => toRows(value), [value]);
50
+
51
+ const emit = (next: OptionRow[]) => onChange(next);
52
+
53
+ const setRow = (index: number, patch: Partial<OptionRow>) => {
54
+ emit(rows.map((row, i) => (i === index ? { ...row, ...patch } : row)));
55
+ };
56
+
57
+ const addRow = () => {
58
+ emit([...rows, { value: '', label: '' }]);
59
+ };
60
+
61
+ const removeRow = (index: number) => {
62
+ emit(rows.filter((_, i) => i !== index));
63
+ };
64
+
65
+ const moveRow = (index: number, dir: -1 | 1) => {
66
+ const next = [...rows];
67
+ const target = index + dir;
68
+ if (target < 0 || target >= next.length) {
69
+ return;
70
+ }
71
+ [next[index], next[target]] = [next[target], next[index]];
72
+ emit(next);
73
+ };
74
+
75
+ return (
76
+ <div style={{ width: '100%' }}>
77
+ {rows.map((row, index) => (
78
+ <div
79
+ key={index}
80
+ style={{
81
+ display: 'flex',
82
+ gap: 6,
83
+ alignItems: 'center',
84
+ marginBottom: 6,
85
+ }}
86
+ >
87
+ <Input
88
+ size='small'
89
+ value={row.value == null ? '' : String(row.value)}
90
+ placeholder='选项值 value'
91
+ onChange={(e) =>
92
+ setRow(index, { value: parseValue(e.target.value) })
93
+ }
94
+ />
95
+ <Input
96
+ size='small'
97
+ value={row.label}
98
+ placeholder='选项文案 label'
99
+ onChange={(e) => setRow(index, { label: e.target.value })}
100
+ />
101
+ <Space.Compact>
102
+ <Button
103
+ size='small'
104
+ type='text'
105
+ disabled={index === 0}
106
+ onClick={() => moveRow(index, -1)}
107
+ >
108
+
109
+ </Button>
110
+ <Button
111
+ size='small'
112
+ type='text'
113
+ disabled={index === rows.length - 1}
114
+ onClick={() => moveRow(index, 1)}
115
+ >
116
+
117
+ </Button>
118
+ <Button
119
+ size='small'
120
+ type='text'
121
+ danger
122
+ onClick={() => removeRow(index)}
123
+ >
124
+
125
+ </Button>
126
+ </Space.Compact>
127
+ </div>
128
+ ))}
129
+ <Button size='small' type='dashed' block onClick={addRow}>
130
+ + 添加选项
131
+ </Button>
132
+ {rows.length === 0 && (
133
+ <div style={{ fontSize: 11, color: '#999', marginTop: 6 }}>
134
+ value 为存入 formData 的选项值,label 为界面展示文案,每行一组
135
+ </div>
136
+ )}
137
+ </div>
138
+ );
139
+ }
140
+
141
+ export const optionsEditorWidget = OptionsEditor;