@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,532 @@
1
+ // ============================================================================
2
+ // ReactionsEditor — 可视化编辑 reactions 联动规则
3
+ // 对应 schema 节点的 reactions 数组(Reaction 协议,见 core/types/schema.ts):
4
+ // { dependencies, when?, fulfill?: { state?, schema? }, otherwise?: { state?, schema? } }
5
+ // - _autoExpr 自动生成的 reaction(来自 required/hidden/disabled/readOnly 表达式)
6
+ // 不可在此编辑(解析时会自动重建),仅展示数量提示
7
+ // - 纯数据转换在 ./reactionsModel 中(可单测)
8
+ // ============================================================================
9
+
10
+ import type { WidgetProps } from '@xbeeant/form-engine-ui';
11
+ import { Button, Form, Input, Select, Switch } from 'antd';
12
+ import { useEffect, useRef, useState } from 'react';
13
+ import { ExpressionBuilder } from './ExpressionBuilder';
14
+ import {
15
+ BOOLEAN_STATE_KEYS,
16
+ type Card,
17
+ cardToReaction,
18
+ genId,
19
+ type PatchRow,
20
+ PROPS_KEY,
21
+ parsePatchValue,
22
+ reactionToCard,
23
+ STATE_KEY_OPTIONS,
24
+ toManualReactions,
25
+ } from './reactionsModel';
26
+ import {
27
+ useFormDataFieldOptions,
28
+ useFormDataFields,
29
+ } from './useFormDataFields';
30
+
31
+ function depsCountOf(deps: string): number {
32
+ return deps
33
+ .split(',')
34
+ .map((s) => s.trim())
35
+ .filter(Boolean).length;
36
+ }
37
+
38
+ // ────────────────────────────────────────────────────────────────────────────
39
+ // InsertVariableSelect — 向文本中追加一个变量表达式(用于值型补丁)
40
+ // ────────────────────────────────────────────────────────────────────────────
41
+
42
+ function InsertVariableSelect({
43
+ fields,
44
+ fieldOptions,
45
+ depsCount,
46
+ onInsert,
47
+ }: {
48
+ fields: string[];
49
+ fieldOptions?: Array<{ value: string; label: string }>;
50
+ depsCount: number;
51
+ onInsert: (text: string) => void;
52
+ }) {
53
+ const base =
54
+ fieldOptions && fieldOptions.length > 0
55
+ ? fieldOptions
56
+ : fields.map((f) => ({ value: f, label: f }));
57
+ const options = [
58
+ ...base.map((o) => ({ value: `formData.${o.value}`, label: o.label })),
59
+ { value: '$self.value', label: '$self.value' },
60
+ ...(depsCount > 0
61
+ ? Array.from({ length: depsCount }, (_, i) => ({
62
+ value: `$deps[${i}]`,
63
+ label: `$deps[${i}]`,
64
+ }))
65
+ : []),
66
+ { value: '$index', label: '$index' },
67
+ { value: 'rootValue', label: 'rootValue' },
68
+ ];
69
+ return (
70
+ <Select
71
+ size='small'
72
+ placeholder='插入变量'
73
+ value={undefined}
74
+ onChange={(v) => {
75
+ if (v) {
76
+ onInsert(v as string);
77
+ }
78
+ }}
79
+ options={options}
80
+ style={{ width: 104 }}
81
+ />
82
+ );
83
+ }
84
+
85
+ // ────────────────────────────────────────────────────────────────────────────
86
+ // PatchValueInput — 单条状态补丁的值编辑器
87
+ // 布尔型状态 key:静态 Switch ⇄ 表达式 Input;其余 key:文本 Input
88
+ // ────────────────────────────────────────────────────────────────────────────
89
+
90
+ function PatchValueInput({
91
+ rowKey,
92
+ value,
93
+ onChange,
94
+ fields,
95
+ fieldOptions,
96
+ depsCount,
97
+ }: {
98
+ rowKey: string;
99
+ value: unknown;
100
+ onChange: (v: unknown) => void;
101
+ fields: string[];
102
+ fieldOptions?: Array<{ value: string; label: string }>;
103
+ depsCount: number;
104
+ }) {
105
+ const booleanish = BOOLEAN_STATE_KEYS.has(rowKey);
106
+ const isExpr = typeof value === 'string' && value.trim().startsWith('{{');
107
+
108
+ if (booleanish) {
109
+ if (isExpr) {
110
+ return (
111
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
112
+ <ExpressionBuilder
113
+ value={value as string}
114
+ onChange={(s) => onChange(s)}
115
+ fields={fields}
116
+ fieldOptions={fieldOptions}
117
+ depsCount={depsCount}
118
+ emitEmpty='empty'
119
+ />
120
+ <div>
121
+ <Button
122
+ size='small'
123
+ type='text'
124
+ onClick={() => onChange(false)}
125
+ style={{ fontSize: 11, padding: '0 4px', height: 'auto' }}
126
+ >
127
+ 改为静态
128
+ </Button>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
133
+ return (
134
+ <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
135
+ <Switch checked={!!value} onChange={(v) => onChange(v)} />
136
+ <Button size='small' onClick={() => onChange('{{ }}')}>
137
+ 表达式
138
+ </Button>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ const insertVariable = (v: string) => {
144
+ const trimmed = String(value ?? '').trim();
145
+ if (
146
+ typeof value === 'string' &&
147
+ value.trim().startsWith('{{') &&
148
+ value.trim().endsWith('}}')
149
+ ) {
150
+ const inner = value.trim().slice(2, -2).trim();
151
+ onChange(`{{ ${inner} ${v} }}`);
152
+ return;
153
+ }
154
+ onChange(parsePatchValue(trimmed ? `${trimmed} ${v}` : v));
155
+ };
156
+
157
+ return (
158
+ <div style={{ display: 'flex', gap: 4, alignItems: 'center', flex: 1 }}>
159
+ <Input
160
+ size='small'
161
+ value={String(value ?? '')}
162
+ onChange={(e) => onChange(parsePatchValue(e.target.value))}
163
+ style={{ flex: 1 }}
164
+ />
165
+ <InsertVariableSelect
166
+ fields={fields}
167
+ fieldOptions={fieldOptions}
168
+ depsCount={depsCount}
169
+ onInsert={insertVariable}
170
+ />
171
+ </div>
172
+ );
173
+ }
174
+
175
+ // ────────────────────────────────────────────────────────────────────────────
176
+ // ReactionsEditor — 主组件(本地工作副本 + 每次编辑提交,避免受控重置)
177
+ // ────────────────────────────────────────────────────────────────────────────
178
+
179
+ export function ReactionsEditor({ value, onChange, title }: WidgetProps) {
180
+ const [cards, setCards] = useState<Card[]>(() =>
181
+ toManualReactions(value).map(reactionToCard),
182
+ );
183
+ const prevValueRef = useRef(value);
184
+ // 记录最近一次由本组件 emit 出去的值:自身 onChange 引发的 value 回传
185
+ // (引擎值更新 → value prop 更新)不应触发「外部 value 变化重置」,
186
+ // 否则点击「添加联动规则」后,undefined → [] 的值变化会立刻清空本地卡片。
187
+ const lastEmittedRef = useRef<unknown>(value);
188
+
189
+ // 外部 value 变化(切换节点 / 属性表单重新初始化)时重置工作副本
190
+ useEffect(() => {
191
+ if (prevValueRef.current !== value) {
192
+ prevValueRef.current = value;
193
+ if (value !== lastEmittedRef.current) {
194
+ setCards(toManualReactions(value).map(reactionToCard));
195
+ }
196
+ }
197
+ }, [value]);
198
+
199
+ const commit = (next: Card[]) => {
200
+ setCards(next);
201
+ const reactions = next
202
+ .map(cardToReaction)
203
+ .filter((r) => r.dependencies.length > 0 || r.fulfill || r.otherwise);
204
+ const emitted = reactions.length > 0 ? reactions : [];
205
+ lastEmittedRef.current = emitted;
206
+ onChange(emitted);
207
+ };
208
+
209
+ const updateCard = (index: number, patch: Partial<Card>) => {
210
+ commit(cards.map((c, i) => (i === index ? { ...c, ...patch } : c)));
211
+ };
212
+
213
+ const removeCard = (index: number) => {
214
+ commit(cards.filter((_, i) => i !== index));
215
+ };
216
+
217
+ const addCard = () => {
218
+ commit([
219
+ ...cards,
220
+ {
221
+ id: genId(),
222
+ deps: '',
223
+ when: '',
224
+ fulfillEnabled: true,
225
+ fulfillRows: [],
226
+ fulfillSchema: '',
227
+ otherwiseEnabled: false,
228
+ otherwiseRows: [],
229
+ otherwiseSchema: '',
230
+ },
231
+ ]);
232
+ };
233
+
234
+ const autoExprCount = Array.isArray(value)
235
+ ? value.filter(
236
+ (r) => !!r && (r as { _autoExpr?: boolean })._autoExpr === true,
237
+ ).length
238
+ : 0;
239
+
240
+ return (
241
+ <Form.Item layout={'vertical'} label={title} style={{ width: '100%' }}>
242
+ {autoExprCount > 0 && (
243
+ <div
244
+ style={{
245
+ fontSize: 11,
246
+ color: '#999',
247
+ marginBottom: 8,
248
+ background: '#fafafa',
249
+ border: '1px solid #f0f0f0',
250
+ borderRadius: 6,
251
+ padding: '4px 8px',
252
+ }}
253
+ >
254
+ 另有 {autoExprCount} 条由 required / hidden / disabled / readOnly
255
+ 表达式自动生成的联动(_autoExpr),请通过「通用属性 / 校验配置」编辑。
256
+ </div>
257
+ )}
258
+
259
+ {cards.map((card, cardIndex) => (
260
+ <ReactionCard
261
+ key={card.id}
262
+ card={card}
263
+ index={cardIndex}
264
+ onChange={updateCard}
265
+ onRemove={removeCard}
266
+ />
267
+ ))}
268
+
269
+ <Button size='small' type='dashed' block onClick={addCard}>
270
+ + 添加联动规则
271
+ </Button>
272
+ <div style={{ fontSize: 11, color: '#999', marginTop: 4 }}>
273
+ 依赖字段值变化时触发本规则。可用变量:$deps(按 dependencies
274
+ 顺序)、$self.value、formData。满足 when(可选)执行 fulfill,否则执行
275
+ otherwise。
276
+ </div>
277
+ </Form.Item>
278
+ );
279
+ }
280
+
281
+ // ────────────────────────────────────────────────────────────────────────────
282
+ // ReactionCard — 单条联动规则
283
+ // ────────────────────────────────────────────────────────────────────────────
284
+
285
+ function ReactionCard({
286
+ card,
287
+ index,
288
+ onChange,
289
+ onRemove,
290
+ }: {
291
+ card: Card;
292
+ index: number;
293
+ onChange: (index: number, patch: Partial<Card>) => void;
294
+ onRemove: (index: number) => void;
295
+ }) {
296
+ const fields = useFormDataFields();
297
+ const fieldOptions = useFormDataFieldOptions();
298
+ const depsCount = depsCountOf(card.deps);
299
+ const depsTags = card.deps
300
+ .split(',')
301
+ .map((s) => s.trim())
302
+ .filter(Boolean);
303
+
304
+ return (
305
+ <div
306
+ style={{
307
+ border: '1px solid #e8e8e8',
308
+ borderRadius: 8,
309
+ padding: 8,
310
+ marginBottom: 8,
311
+ background: '#fff',
312
+ }}
313
+ >
314
+ <div
315
+ style={{
316
+ display: 'flex',
317
+ alignItems: 'center',
318
+ justifyContent: 'space-between',
319
+ marginBottom: 6,
320
+ }}
321
+ >
322
+ <span style={{ fontSize: 12, fontWeight: 600, color: '#333' }}>
323
+ 联动 {index + 1}
324
+ </span>
325
+ <Button size='small' danger onClick={() => onRemove(index)}>
326
+ 删除
327
+ </Button>
328
+ </div>
329
+
330
+ <div style={{ marginBottom: 6 }}>
331
+ <div style={{ fontSize: 11, color: '#666', marginBottom: 2 }}>
332
+ 依赖字段(dependencies)
333
+ </div>
334
+ <Select
335
+ size='small'
336
+ mode='tags'
337
+ value={depsTags}
338
+ onChange={(tags) => onChange(index, { deps: tags.join(', ') })}
339
+ options={fieldOptions}
340
+ placeholder='选择或输入依赖字段'
341
+ style={{ width: '100%' }}
342
+ />
343
+ </div>
344
+
345
+ <div style={{ marginBottom: 6 }}>
346
+ <div style={{ fontSize: 11, color: '#666', marginBottom: 2 }}>
347
+ 条件(when,可选)
348
+ </div>
349
+ <ExpressionBuilder
350
+ value={card.when}
351
+ onChange={(s) => onChange(index, { when: s })}
352
+ fields={fields}
353
+ fieldOptions={fieldOptions}
354
+ depsCount={depsCount}
355
+ emitEmpty='empty'
356
+ placeholder='{{ $deps[0] === true }}'
357
+ />
358
+ </div>
359
+
360
+ <BranchEditor
361
+ title='满足时(fulfill)'
362
+ enabled={card.fulfillEnabled}
363
+ rows={card.fulfillRows}
364
+ schemaText={card.fulfillSchema}
365
+ fields={fields}
366
+ fieldOptions={fieldOptions}
367
+ depsCount={depsCount}
368
+ onToggle={(v) => onChange(index, { fulfillEnabled: v })}
369
+ onRows={(rows) => onChange(index, { fulfillRows: rows })}
370
+ onSchema={(t) => onChange(index, { fulfillSchema: t })}
371
+ />
372
+
373
+ <BranchEditor
374
+ title='不满足时(otherwise)'
375
+ enabled={card.otherwiseEnabled}
376
+ rows={card.otherwiseRows}
377
+ schemaText={card.otherwiseSchema}
378
+ fields={fields}
379
+ fieldOptions={fieldOptions}
380
+ depsCount={depsCount}
381
+ onToggle={(v) => onChange(index, { otherwiseEnabled: v })}
382
+ onRows={(rows) => onChange(index, { otherwiseRows: rows })}
383
+ onSchema={(t) => onChange(index, { otherwiseSchema: t })}
384
+ />
385
+ </div>
386
+ );
387
+ }
388
+
389
+ // ────────────────────────────────────────────────────────────────────────────
390
+ // BranchEditor — fulfill / otherwise 分支:状态补丁 + 属性补丁
391
+ // ────────────────────────────────────────────────────────────────────────────
392
+
393
+ function BranchEditor({
394
+ title,
395
+ enabled,
396
+ rows,
397
+ schemaText,
398
+ fields,
399
+ fieldOptions,
400
+ depsCount,
401
+ onToggle,
402
+ onRows,
403
+ onSchema,
404
+ }: {
405
+ title: string;
406
+ enabled: boolean;
407
+ rows: PatchRow[];
408
+ schemaText: string;
409
+ fields: string[];
410
+ fieldOptions?: Array<{ value: string; label: string }>;
411
+ depsCount: number;
412
+ onToggle: (enabled: boolean) => void;
413
+ onRows: (rows: PatchRow[]) => void;
414
+ onSchema: (text: string) => void;
415
+ }) {
416
+ const updateRow = (idx: number, patch: Partial<PatchRow>) => {
417
+ onRows(rows.map((r, i) => (i === idx ? { ...r, ...patch } : r)));
418
+ };
419
+
420
+ const addRow = () => {
421
+ onRows([...rows, { key: 'required', value: true }]);
422
+ };
423
+
424
+ const removeRow = (idx: number) => {
425
+ onRows(rows.filter((_, i) => i !== idx));
426
+ };
427
+
428
+ return (
429
+ <div style={{ marginBottom: 6 }}>
430
+ <div
431
+ style={{
432
+ display: 'flex',
433
+ alignItems: 'center',
434
+ justifyContent: 'space-between',
435
+ marginBottom: 4,
436
+ }}
437
+ >
438
+ <span style={{ fontSize: 11, color: '#666' }}>{title}</span>
439
+ <Switch size='small' checked={enabled} onChange={(v) => onToggle(v)} />
440
+ </div>
441
+
442
+ {enabled && (
443
+ <>
444
+ {rows.map((row, idx) => (
445
+ <div
446
+ key={`${idx}-${row.key}`}
447
+ style={{
448
+ display: 'flex',
449
+ gap: 4,
450
+ alignItems: 'center',
451
+ marginBottom: 4,
452
+ }}
453
+ >
454
+ <PatchKeySelect
455
+ row={row}
456
+ onChange={(key) => updateRow(idx, { key })}
457
+ />
458
+ <div style={{ flex: 1 }}>
459
+ <PatchValueInput
460
+ rowKey={row.key}
461
+ value={row.value}
462
+ onChange={(v) => updateRow(idx, { value: v })}
463
+ fields={fields}
464
+ fieldOptions={fieldOptions}
465
+ depsCount={depsCount}
466
+ />
467
+ </div>
468
+ <Button size='small' onClick={() => removeRow(idx)}>
469
+ ×
470
+ </Button>
471
+ </div>
472
+ ))}
473
+ <Button
474
+ size='small'
475
+ block
476
+ style={{ marginBottom: 4 }}
477
+ onClick={addRow}
478
+ >
479
+ + 添加状态项
480
+ </Button>
481
+
482
+ <div style={{ fontSize: 11, color: '#666', marginBottom: 2 }}>
483
+ 属性补丁(schema,JSON,可选)
484
+ </div>
485
+ <Input.TextArea
486
+ size='small'
487
+ rows={2}
488
+ placeholder={'{\n "props.options": [...]\n}'}
489
+ value={schemaText}
490
+ onChange={(e) => onSchema(e.target.value)}
491
+ />
492
+ </>
493
+ )}
494
+ </div>
495
+ );
496
+ }
497
+
498
+ // ────────────────────────────────────────────────────────────────────────────
499
+ // PatchKeySelect — 状态补丁的 key 选择器(含 props.* 自定义路径)
500
+ // ────────────────────────────────────────────────────────────────────────────
501
+
502
+ function PatchKeySelect({
503
+ row,
504
+ onChange,
505
+ }: {
506
+ row: PatchRow;
507
+ onChange: (key: string) => void;
508
+ }) {
509
+ const isProps = row.key.startsWith('props.');
510
+ return (
511
+ <div style={{ display: 'flex', gap: 4 }}>
512
+ <Select
513
+ size='small'
514
+ value={isProps ? PROPS_KEY : row.key}
515
+ options={STATE_KEY_OPTIONS}
516
+ onChange={(k) => onChange(k === PROPS_KEY ? 'props.' : k)}
517
+ style={{ width: 132 }}
518
+ />
519
+ {isProps && (
520
+ <Input
521
+ size='small'
522
+ placeholder='如 options'
523
+ value={row.key.slice('props.'.length)}
524
+ onChange={(e) => onChange(`props.${e.target.value}`)}
525
+ style={{ width: 96 }}
526
+ />
527
+ )}
528
+ </div>
529
+ );
530
+ }
531
+
532
+ export const reactionsEditorWidget = ReactionsEditor;