@dckj-npm/dc-material 0.1.377 → 0.1.379

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/build/docs/colorful-button.html +3 -3
  2. package/build/docs/colorful-input.html +3 -3
  3. package/build/docs/custom-form/requirements.html +48 -0
  4. package/build/docs/custom-form.html +48 -0
  5. package/build/docs/index.html +3 -3
  6. package/build/docs/teletext-list.html +3 -3
  7. package/build/docs/umi.80bcbda5.js +1 -0
  8. package/build/docs/{umi.9770df27.css → umi.b31e14a3.css} +1 -1
  9. package/build/docs/~demos/colorful-button-demo.html +3 -3
  10. package/build/docs/~demos/colorful-input-demo.html +3 -3
  11. package/build/docs/~demos/teletext-list-demo-1.html +3 -3
  12. package/build/docs/~demos/teletext-list-demo.html +3 -3
  13. package/build/lowcode/assets-daily.json +13 -13
  14. package/build/lowcode/assets-dev.json +2 -2
  15. package/build/lowcode/assets-prod.json +13 -13
  16. package/build/lowcode/meta.design.js +1 -1
  17. package/build/lowcode/meta.js +1 -1
  18. package/build/lowcode/render/default/view.css +1 -1
  19. package/build/lowcode/render/default/view.js +1 -1
  20. package/build/lowcode/view.css +1 -1
  21. package/build/lowcode/view.js +1 -1
  22. package/dist/BizComps.css +1 -1
  23. package/dist/BizComps.js +2 -2
  24. package/dist/BizComps.js.map +1 -1
  25. package/es/components/custom-form/CUSTOM_FORM_OPERATION_MANUAL.md +284 -0
  26. package/es/components/custom-form/custom-form.d.ts +188 -2
  27. package/es/components/custom-form/custom-form.js +506 -26
  28. package/es/components/custom-form/index.d.ts +1 -1
  29. package/es/components/custom-form/index.scss +1 -1
  30. package/es/components/custom-form/schema.json +1374 -0
  31. package/lib/components/custom-form/CUSTOM_FORM_OPERATION_MANUAL.md +284 -0
  32. package/lib/components/custom-form/custom-form.d.ts +188 -2
  33. package/lib/components/custom-form/custom-form.js +511 -31
  34. package/lib/components/custom-form/index.d.ts +1 -1
  35. package/lib/components/custom-form/index.scss +1 -1
  36. package/lib/components/custom-form/schema.json +1374 -0
  37. package/lowcode/custom-form/meta.ts +1675 -116
  38. package/lowcode_es/custom-form/meta.js +2116 -141
  39. package/lowcode_es/meta.js +1 -1
  40. package/lowcode_lib/custom-form/meta.js +2116 -141
  41. package/lowcode_lib/meta.js +1 -1
  42. package/package.json +3 -3
  43. package/build/docs/umi.6f6bf535.js +0 -1
@@ -1,4 +1,4 @@
1
- import { IPublicTypeComponentMetadata, IPublicTypeSnippet } from '@alilc/lowcode-types'
1
+ import { IPublicTypeComponentMetadata, IPublicTypeSnippet } from '@alilc/lowcode-types'
2
2
 
3
3
  const CustomFormMeta: IPublicTypeComponentMetadata = {
4
4
  group: '低代码组件',
@@ -21,48 +21,180 @@ const CustomFormMeta: IPublicTypeComponentMetadata = {
21
21
  isContainer: true,
22
22
  },
23
23
  props: [
24
+ // ─────────────────────────────────────────────────────────────────────
25
+ // 分组一:全局布局
26
+ // ─────────────────────────────────────────────────────────────────────
24
27
  {
25
- title: '列数',
26
- name: 'columns',
27
- setter: {
28
- componentName: 'RadioGroupSetter',
29
- props: {
30
- dataSource: [
31
- { label: '一列', value: 1 },
32
- { label: '二列', value: 2 },
33
- { label: '三列', value: 3 },
34
- { label: '四列', value: 4 },
35
- ],
36
- options: [
37
- { label: '一列', value: 1 },
38
- { label: '二列', value: 2 },
39
- { label: '三列', value: 3 },
40
- { label: '四列', value: 4 },
41
- ],
28
+ type: 'group',
29
+ title: '全局布局',
30
+ name: 'layoutGroup',
31
+ display: 'accordion',
32
+ items: [
33
+ {
34
+ title: {
35
+ label: '列数',
36
+ tip: '表单的整体列数,所有表单项按此列数排列。\n单列适合简单表单,多列适合信息密集的编辑页面。',
37
+ },
38
+ name: 'columns',
39
+ setter: {
40
+ componentName: 'RadioGroupSetter',
41
+ props: {
42
+ options: [
43
+ { title: '一列', value: 1 },
44
+ { title: '二列', value: 2 },
45
+ { title: '三列', value: 3 },
46
+ { title: '四列', value: 4 },
47
+ ],
48
+ },
49
+ initialValue: 1,
50
+ },
42
51
  },
43
- initialValue: 1,
44
- },
45
- },
46
- {
47
- title: '间距',
48
- name: 'spacing',
49
- setter: {
50
- componentName: 'JsonSetter',
51
- isRequired: false,
52
- initialValue: [0, 16, 16, 0],
53
- },
52
+ {
53
+ title: {
54
+ label: '标签位置',
55
+ tip: '表单标签相对于输入框的位置:\n· 顶部(top):标签在输入框上方,适合移动端或字段较多时\n· 左侧(left):标签在输入框左侧,传统表单样式,配合「标签宽度」使用\n· 内嵌(inset):标签显示在输入框内部,输入时标签浮动到上方',
56
+ },
57
+ name: 'labelAlign',
58
+ setter: {
59
+ componentName: 'RadioGroupSetter',
60
+ props: {
61
+ options: [
62
+ { title: '顶部', value: 'top' },
63
+ { title: '左侧', value: 'left' },
64
+ { title: '内嵌', value: 'inset' },
65
+ ],
66
+ },
67
+ initialValue: 'top',
68
+ },
69
+ extraProps: {
70
+ setValue: (target: any, value: string) => {
71
+ if (value === 'left') {
72
+ target.getProps().setPropValue('labelCol', { fixedSpan: 4 })
73
+ } else {
74
+ target.getProps().setPropValue('labelCol', null)
75
+ }
76
+ target.getProps().setPropValue('labelAlign', value)
77
+ },
78
+ },
79
+ },
80
+ {
81
+ title: {
82
+ label: '标签宽度',
83
+ tip: '当标签位置为「左侧」时,设置标签列的固定宽度(格宫列数,1~24)。\nJSON 示例:{ "fixedSpan": 6 } 意为标签占 6 格。\n不填则自动宽度。常用值:4(短标签)、6(中等标签)、8(长标签)',
84
+ },
85
+ name: 'labelCol.fixedSpan',
86
+ condition: (target: any) =>
87
+ target.getProps().getPropValue('labelAlign') === 'left',
88
+ setter: {
89
+ componentName: 'NumberSetter',
90
+ props: { min: 1, max: 12 },
91
+ initialValue: 4,
92
+ },
93
+ },
94
+ {
95
+ title: {
96
+ label: '行间距',
97
+ tip: '每行表单项之间的上下间距(像素)。\n默认 0。建议设为 8~16px 让表单更舒适。',
98
+ },
99
+ name: '!rowGap',
100
+ extraProps: {
101
+ getValue: (target: any) => {
102
+ const sp = target.getProps().getPropValue('spacing')
103
+ return Array.isArray(sp) ? (sp[0] ?? 0) : 0
104
+ },
105
+ setValue: (target: any, value: number) => {
106
+ const sp = target.getProps().getPropValue('spacing')
107
+ const colGap = Array.isArray(sp) ? (sp[1] ?? 16) : 16
108
+ target.getProps().setPropValue('spacing', [value, colGap])
109
+ },
110
+ },
111
+ setter: {
112
+ componentName: 'NumberSetter',
113
+ props: { min: 0, max: 100 },
114
+ initialValue: 0,
115
+ },
116
+ },
117
+ {
118
+ title: {
119
+ label: '列间距',
120
+ tip: '多列表单中各列之间的左右间距(像素)。\n默认 16px。单列表单中此设置无明显效果。',
121
+ },
122
+ name: '!colGap',
123
+ extraProps: {
124
+ getValue: (target: any) => {
125
+ const sp = target.getProps().getPropValue('spacing')
126
+ return Array.isArray(sp) ? (sp[1] ?? 16) : 16
127
+ },
128
+ setValue: (target: any, value: number) => {
129
+ const sp = target.getProps().getPropValue('spacing')
130
+ const rowGap = Array.isArray(sp) ? (sp[0] ?? 0) : 0
131
+ target.getProps().setPropValue('spacing', [rowGap, value])
132
+ },
133
+ },
134
+ setter: {
135
+ componentName: 'NumberSetter',
136
+ props: { min: 0, max: 100 },
137
+ initialValue: 16,
138
+ },
139
+ },
140
+ {
141
+ title: {
142
+ label: '组件宽度占满',
143
+ tip: '全局控制所有表单项的输入组件是否撑满容器宽度(100%)。\n开启后相当为每个表单项设置 fullWidth=true。\n单个表单项的「宽度占满」设置优先级更高,可覆盖此全局值。',
144
+ },
145
+ name: 'fullWidth',
146
+ setter: {
147
+ componentName: 'BoolSetter',
148
+ initialValue: true,
149
+ },
150
+ },
151
+ {
152
+ title: {
153
+ label: '表单状态',
154
+ tip: '编辑态:正常填写表单。\n只读态:所有输入框变为纯文本显示,空值显示"—"。\n常用于"查看详情"场景,绑定变量可动态切换编辑/只读。',
155
+ },
156
+ name: '!status',
157
+ extraProps: {
158
+ getValue: (target: any) => {
159
+ return target.getProps().getPropValue('isPreview') ? 'readonly' : 'editable'
160
+ },
161
+ setValue: (target: any, value: string) => {
162
+ target.getProps().setPropValue('isPreview', value === 'readonly')
163
+ },
164
+ },
165
+ setter: {
166
+ componentName: 'RadioGroupSetter',
167
+ props: {
168
+ options: [
169
+ { title: '编辑', value: 'editable' },
170
+ { title: '只读', value: 'readonly' },
171
+ ],
172
+ },
173
+ initialValue: 'editable',
174
+ },
175
+ },
176
+ {
177
+ title: {
178
+ label: '空态文案',
179
+ tip: '当没有任何表单项时展示的提示文字。',
180
+ },
181
+ name: 'emptyContent',
182
+ setter: {
183
+ componentName: 'StringSetter',
184
+ isRequired: false,
185
+ initialValue: '添加表单项',
186
+ },
187
+ },
188
+ ],
54
189
  },
190
+ // ─────────────────────────────────────────────────────────────────────
191
+ // 分组二:表单项配置
192
+ // ─────────────────────────────────────────────────────────────────────
55
193
  {
56
- title: '空态文案',
57
- name: 'emptyContent',
58
- setter: {
59
- componentName: 'StringSetter',
60
- isRequired: false,
61
- initialValue: '添加表单项',
194
+ title: {
195
+ label: '表单项',
196
+ tip: '配置表单中每一行的字段。\n每个表单项需要填写「字段名」和「标题」,其余可选。\n字段名(field)是提交数据时的 key,需要与后端接口字段名保持一致。',
62
197
  },
63
- },
64
- {
65
- title: '表单项',
66
198
  name: 'formItems',
67
199
  setter: {
68
200
  componentName: 'ArraySetter',
@@ -72,8 +204,12 @@ const CustomFormMeta: IPublicTypeComponentMetadata = {
72
204
  props: {
73
205
  config: {
74
206
  items: [
207
+ // ── 基础
75
208
  {
76
- title: '字段名',
209
+ title: {
210
+ label: '字段名',
211
+ tip: '必填。提交表单时该字段的 key 名称,需与后端接口字段名一致。\n例如:user_name、phone、banquet_date',
212
+ },
77
213
  name: 'field',
78
214
  setter: {
79
215
  componentName: 'StringSetter',
@@ -82,8 +218,13 @@ const CustomFormMeta: IPublicTypeComponentMetadata = {
82
218
  },
83
219
  },
84
220
  {
85
- title: '标题',
221
+ title: {
222
+ label: '标题',
223
+ tip: '表单项左侧(或上方)显示的文字标签。\n例如:姓名、手机号、宴请日期',
224
+ },
86
225
  name: 'label',
226
+ important: true,
227
+ display: 'inline',
87
228
  setter: {
88
229
  componentName: 'StringSetter',
89
230
  isRequired: false,
@@ -91,7 +232,10 @@ const CustomFormMeta: IPublicTypeComponentMetadata = {
91
232
  },
92
233
  },
93
234
  {
94
- title: '必填',
235
+ title: {
236
+ label: '必填',
237
+ tip: '开启后标题旁显示红色星号,并在提交时自动校验该字段不能为空。',
238
+ },
95
239
  name: 'required',
96
240
  setter: {
97
241
  componentName: 'BoolSetter',
@@ -99,29 +243,81 @@ const CustomFormMeta: IPublicTypeComponentMetadata = {
99
243
  initialValue: false,
100
244
  },
101
245
  },
246
+ // ── 组件类型
102
247
  {
103
- title: '组件类型',
248
+ title: {
249
+ label: '组件类型',
250
+ tip: '选择该字段使用哪种输入控件:\n· 输入框(Input):单行文本,最常用\n· 多行文本(TextArea):适合备注、描述\n· 下拉选择(Select):从预设选项中选一个\n· 单选组(RadioGroup):横排单选按钮\n· 复选组(CheckboxGroup):多选框\n· 数字输入(NumberPicker):带加减按钮的数字\n· 日期选择(DatePicker):日历弹出选日期\n· 上传(Upload):文件/图片上传',
251
+ },
104
252
  name: 'componentType',
253
+ important: true,
254
+ display: 'inline',
105
255
  setter: {
106
256
  componentName: 'SelectSetter',
107
257
  props: {
108
258
  options: [
109
- { label: '输入框', value: 'Input' },
110
- { label: '多行文本', value: 'TextArea' },
111
- { label: '下拉选择', value: 'Select' },
112
- { label: '单选组', value: 'RadioGroup' },
113
- { label: '复选组', value: 'CheckboxGroup' },
114
- { label: '数字输入', value: 'NumberPicker' },
115
- { label: '日期选择', value: 'DatePicker' },
116
- { label: '上传', value: 'Upload' },
259
+ { label: '输入框 (Input)', value: 'Input' },
260
+ { label: '多行文本 (TextArea)', value: 'TextArea' },
261
+ { label: '下拉选择 (Select)', value: 'Select' },
262
+ { label: '单选组 (RadioGroup)', value: 'RadioGroup' },
263
+ { label: '复选组 (CheckboxGroup)', value: 'CheckboxGroup' },
264
+ { label: '数字输入 (NumberPicker)', value: 'NumberPicker' },
265
+ { label: '日期选择 (DatePicker)', value: 'DatePicker' },
266
+ { label: '日期时间 (DateTimePicker)', value: 'DateTimePicker' },
267
+ { label: '上传 (Upload)', value: 'Upload' },
117
268
  ],
118
269
  },
119
270
  initialValue: 'Input',
120
271
  },
121
272
  },
273
+ // ── 输入提示(仅对支持 placeholder 的组件显示)
122
274
  {
123
- title: '选项',
275
+ title: {
276
+ label: '输入提示',
277
+ tip: '显示在输入框内的灰色提示文字(placeholder)。\n例如:请输入姓名、请选择日期',
278
+ },
279
+ name: 'placeholder',
280
+ condition: (target: any) => {
281
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
282
+ return !['Upload', 'RadioGroup', 'CheckboxGroup'].includes(t)
283
+ },
284
+ setter: {
285
+ componentName: 'StringSetter',
286
+ isRequired: false,
287
+ initialValue: '',
288
+ },
289
+ },
290
+ // ── 默认值
291
+ {
292
+ title: {
293
+ label: '默认值',
294
+ tip: '该字段的初始默认值,表单打开时自动填入。\n· 文本类型填写字符串,例如:待处理\n· 数字类型填写数字,例如:0\n· 日期类型填写格式化字符串,例如:2026-01-01\n· 下拉/单选填写选项的 value 值,例如:male\n· 复选框填写数组,例如:["a","b"]\n\n优先级高于"全局初始值"(initialValues)中同名字段。',
295
+ },
296
+ name: 'initialValue',
297
+ setter: {
298
+ componentName: 'MixedSetter',
299
+ props: {
300
+ setters: [
301
+ { componentName: 'StringSetter', title: '字符串' },
302
+ { componentName: 'NumberSetter', title: '数字' },
303
+ { componentName: 'BoolSetter', title: '布尔' },
304
+ { componentName: 'JsonSetter', title: 'JSON(数组/对象)' },
305
+ ],
306
+ },
307
+ isRequired: false,
308
+ },
309
+ },
310
+ // ── 选项(下拉/单选/复选)
311
+ {
312
+ title: {
313
+ label: '选项列表',
314
+ tip: '仅在组件类型为「下拉选择」「单选组」「复选组」时生效。\n每个选项需配置:\n· 名称(label):界面上显示的文字,例如:男\n· 值(value):提交时实际传给后端的值,例如:male',
315
+ },
124
316
  name: 'options',
317
+ condition: (target: any) => {
318
+ const t = target.parent?.getPropValue?.('componentType') || target.parent?.parent?.getPropValue?.('componentType')
319
+ return ['Select','RadioGroup','CheckboxGroup'].includes(t)
320
+ },
125
321
  setter: {
126
322
  componentName: 'ArraySetter',
127
323
  props: {
@@ -131,7 +327,7 @@ const CustomFormMeta: IPublicTypeComponentMetadata = {
131
327
  config: {
132
328
  items: [
133
329
  {
134
- title: '名称',
330
+ title: { label: '名称', tip: '选项显示的文字' },
135
331
  name: 'label',
136
332
  setter: {
137
333
  componentName: 'StringSetter',
@@ -140,7 +336,7 @@ const CustomFormMeta: IPublicTypeComponentMetadata = {
140
336
  },
141
337
  },
142
338
  {
143
- title: '值',
339
+ title: { label: '值', tip: '提交时传给后端的实际值' },
144
340
  name: 'value',
145
341
  setter: {
146
342
  componentName: 'StringSetter',
@@ -156,40 +352,684 @@ const CustomFormMeta: IPublicTypeComponentMetadata = {
156
352
  initialValue: [],
157
353
  },
158
354
  },
355
+ // ── 动态选项绑定(阶段二)
159
356
  {
160
- title: '列跨度',
357
+ title: {
358
+ label: '动态选项绑定',
359
+ tip: '仅在组件类型为「下拉选择」「单选组」「复选组」时生效。\n绑定后优先级高于上方「选项列表」(静态配置)。\n支持绑定页面变量或数据源返回的数组,数组格式为 [{label, value}, ...]。\n常见用法:将套餐列表、城市列表等接口数据直接绑定到选项。',
360
+ },
361
+ name: 'optionsBind',
362
+ condition: (target: any) => {
363
+ const t = target.parent?.getPropValue?.('componentType') || target.parent?.parent?.getPropValue?.('componentType')
364
+ return ['Select','RadioGroup','CheckboxGroup'].includes(t)
365
+ },
366
+ setter: {
367
+ componentName: 'SetterFormVariable',
368
+ props: {
369
+ attributes: [
370
+ {
371
+ label: '选项数据',
372
+ value: 'optionsBind',
373
+ children: [
374
+ { label: '显示文字', isRequire: true, value: 'label' },
375
+ { label: '选项值', isRequire: true, value: 'value' },
376
+ ],
377
+ },
378
+ ],
379
+ },
380
+ },
381
+ },
382
+ // ── 布局
383
+ {
384
+ title: {
385
+ label: '列跨度',
386
+ tip: '该表单项横向占几列(不超过表单总列数)。\n例如总列数为 4,设置为 2 则占一半宽度,设置为 4 则独占一行。',
387
+ },
161
388
  name: 'columnSpan',
162
389
  setter: {
163
- componentName: 'NumberSetter',
390
+ componentName: 'RadioGroupSetter',
164
391
  props: {
165
- min: 1,
166
- max: 4,
392
+ options: [
393
+ { title: '1格', value: 1 },
394
+ { title: '2格', value: 2 },
395
+ { title: '3格', value: 3 },
396
+ { title: '4格', value: 4 },
397
+ ],
167
398
  },
168
399
  initialValue: 1,
169
400
  },
170
401
  },
171
402
  {
172
- title: '组件属性',
173
- name: 'componentProps',
403
+ title: {
404
+ label: '尺寸',
405
+ tip: '单个表单项的组件尺寸,优先级高于表单全局 size。\n不设置时继承表单全局尺寸(默认 medium)。',
406
+ },
407
+ name: 'size',
408
+ setter: {
409
+ componentName: 'RadioGroupSetter',
410
+ props: {
411
+ options: [
412
+ { title: '小', value: 'small' },
413
+ { title: '中', value: 'medium' },
414
+ { title: '大', value: 'large' },
415
+ ],
416
+ },
417
+ },
418
+ },
419
+ {
420
+ title: {
421
+ label: '宽度占满',
422
+ tip: '开启后该表单项的输入组件宽度为 100%,撑满所在列的宽度。\n默认开启,通常不需要关闭。',
423
+ },
424
+ name: 'fullWidth',
174
425
  setter: {
175
- componentName: 'JsonSetter',
426
+ componentName: 'BoolSetter',
427
+ initialValue: true,
428
+ },
429
+ },
430
+ // ── 提示信息
431
+ {
432
+ title: {
433
+ label: '错误提示',
434
+ tip: '校验失败时显示的自定义错误文字,涵盖必填为空与正则/格式不匹配两种情况。\n不填则由组件自动生成,例如"xxx 不能为空"或"格式不正确"。\n示例:请输入正确的 11 位手机号',
435
+ },
436
+ name: '!helpMsg',
437
+ extraProps: {
438
+ getValue: (target: any) =>
439
+ target.parent?.getPropValue?.('help') ||
440
+ target.parent?.getPropValue?.('formItemProps')?.requiredMessage ||
441
+ '',
442
+ setValue: (target: any, value: string) => {
443
+ target.parent?.setPropValue?.('help', value || undefined)
444
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
445
+ if (!value) {
446
+ delete fp.requiredMessage
447
+ } else {
448
+ fp.requiredMessage = value
449
+ }
450
+ target.parent?.setPropValue?.('formItemProps', fp)
451
+ },
452
+ },
453
+ setter: {
454
+ componentName: 'StringSetter',
176
455
  isRequired: false,
456
+ initialValue: '',
177
457
  },
178
458
  },
179
459
  {
180
- title: '表单项属性',
181
- name: 'formItemProps',
460
+ title: {
461
+ label: '补充说明',
462
+ tip: '始终显示在输入框下方的灰色提示文字,与错误提示共存,用于告知用户填写要求或格式。\n例如:手机号用于接收预订通知短信',
463
+ },
464
+ name: 'extra',
182
465
  setter: {
183
- componentName: 'JsonSetter',
466
+ componentName: 'StringSetter',
184
467
  isRequired: false,
468
+ initialValue: '',
469
+ },
470
+ },
471
+ // ── 组件属性(结构化,按组件类型分组)
472
+ // ── 通用:禁用
473
+ {
474
+ title: {
475
+ label: '禁用',
476
+ tip: '开启后该输入组件变为禁用状态,用户无法编辑。',
477
+ },
478
+ name: '!disabled',
479
+ condition: (target: any) => {
480
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
481
+ return !['Upload'].includes(t)
482
+ },
483
+ extraProps: {
484
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.disabled,
485
+ setValue: (target: any, value: boolean) => {
486
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
487
+ cp.disabled = value
488
+ target.parent?.setPropValue?.('componentProps', cp)
489
+ },
490
+ },
491
+ setter: { componentName: 'BoolSetter', initialValue: false },
492
+ },
493
+ // ── Input:最大字符数 / 只读
494
+ {
495
+ title: {
496
+ label: '最大字符数',
497
+ tip: '输入框最多允许输入的字符数。\n超出后无法继续输入。配合「显示计数」使用效果更好。',
498
+ },
499
+ name: '!maxLength',
500
+ condition: (target: any) => {
501
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
502
+ return ['Input', 'TextArea'].includes(t)
503
+ },
504
+ extraProps: {
505
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.maxLength,
506
+ setValue: (target: any, value: number) => {
507
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
508
+ if (value === undefined || value === null || (value as any) === '') {
509
+ delete cp.maxLength
510
+ } else {
511
+ cp.maxLength = value
512
+ }
513
+ target.parent?.setPropValue?.('componentProps', cp)
514
+ },
515
+ },
516
+ setter: { componentName: 'NumberSetter', props: { min: 1 } },
517
+ },
518
+ {
519
+ title: {
520
+ label: '显示计数',
521
+ tip: '开启后在输入框右下角显示"已输入/最大字符数"计数器。\n需配合「最大字符数」使用。',
522
+ },
523
+ name: '!hasLimitHint',
524
+ condition: (target: any) => {
525
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
526
+ return ['Input', 'TextArea'].includes(t)
527
+ },
528
+ extraProps: {
529
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.hasLimitHint,
530
+ setValue: (target: any, value: boolean) => {
531
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
532
+ cp.hasLimitHint = value
533
+ target.parent?.setPropValue?.('componentProps', cp)
534
+ },
535
+ },
536
+ setter: { componentName: 'BoolSetter', initialValue: false },
537
+ },
538
+ {
539
+ title: {
540
+ label: '只读',
541
+ tip: '开启后输入框内容不可编辑(与禁用不同,只读不改变样式)。\n常用于"自动计算填入"的字段,如套餐价格。',
542
+ },
543
+ name: '!readOnly',
544
+ condition: (target: any) => {
545
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
546
+ return ['Input', 'TextArea'].includes(t)
547
+ },
548
+ extraProps: {
549
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.readOnly,
550
+ setValue: (target: any, value: boolean) => {
551
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
552
+ cp.readOnly = value
553
+ target.parent?.setPropValue?.('componentProps', cp)
554
+ },
555
+ },
556
+ setter: { componentName: 'BoolSetter', initialValue: false },
557
+ },
558
+ // ── TextArea:行数
559
+ {
560
+ title: {
561
+ label: '显示行数',
562
+ tip: '多行文本框默认显示的行数(高度)。默认 4 行。',
563
+ },
564
+ name: '!rows',
565
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'TextArea',
566
+ extraProps: {
567
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.rows,
568
+ setValue: (target: any, value: number) => {
569
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
570
+ cp.rows = value
571
+ target.parent?.setPropValue?.('componentProps', cp)
572
+ },
573
+ },
574
+ setter: { componentName: 'NumberSetter', props: { min: 1, max: 20 }, initialValue: 4 },
575
+ },
576
+ // ── NumberPicker:范围 / 步长 / 精度
577
+ {
578
+ title: {
579
+ label: '最小值',
580
+ tip: '数字输入框允许输入的最小值。',
581
+ },
582
+ name: '!min',
583
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker',
584
+ extraProps: {
585
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.min,
586
+ setValue: (target: any, value: number) => {
587
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
588
+ if (value === undefined || value === null || (value as any) === '') delete cp.min
589
+ else cp.min = value
590
+ target.parent?.setPropValue?.('componentProps', cp)
591
+ },
592
+ },
593
+ setter: { componentName: 'NumberSetter' },
594
+ },
595
+ {
596
+ title: {
597
+ label: '最大值',
598
+ tip: '数字输入框允许输入的最大值。',
599
+ },
600
+ name: '!max',
601
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker',
602
+ extraProps: {
603
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.max,
604
+ setValue: (target: any, value: number) => {
605
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
606
+ if (value === undefined || value === null || (value as any) === '') delete cp.max
607
+ else cp.max = value
608
+ target.parent?.setPropValue?.('componentProps', cp)
609
+ },
610
+ },
611
+ setter: { componentName: 'NumberSetter' },
612
+ },
613
+ {
614
+ title: {
615
+ label: '步长',
616
+ tip: '每次点击加减按钮时变化的数量。默认 1。',
617
+ },
618
+ name: '!step',
619
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker',
620
+ extraProps: {
621
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.step,
622
+ setValue: (target: any, value: number) => {
623
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
624
+ if (value === undefined || value === null || (value as any) === '') delete cp.step
625
+ else cp.step = value
626
+ target.parent?.setPropValue?.('componentProps', cp)
627
+ },
628
+ },
629
+ setter: { componentName: 'NumberSetter', props: { min: 0 }, initialValue: 1 },
630
+ },
631
+ {
632
+ title: {
633
+ label: '小数位数',
634
+ tip: '数字保留几位小数。默认不限制(整数)。',
635
+ },
636
+ name: '!precision',
637
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker',
638
+ extraProps: {
639
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.precision,
640
+ setValue: (target: any, value: number) => {
641
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
642
+ if (value === undefined || value === null || (value as any) === '') delete cp.precision
643
+ else cp.precision = value
644
+ target.parent?.setPropValue?.('componentProps', cp)
645
+ },
646
+ },
647
+ setter: { componentName: 'NumberSetter', props: { min: 0, max: 10 } },
648
+ },
649
+ // ── Select:可搜索 / 可清除 / 多选模式
650
+ {
651
+ title: {
652
+ label: '可搜索',
653
+ tip: '开启后下拉框支持输入关键字过滤选项。',
654
+ },
655
+ name: '!showSearch',
656
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'Select',
657
+ extraProps: {
658
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.showSearch,
659
+ setValue: (target: any, value: boolean) => {
660
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
661
+ cp.showSearch = value
662
+ target.parent?.setPropValue?.('componentProps', cp)
663
+ },
664
+ },
665
+ setter: { componentName: 'BoolSetter', initialValue: false },
666
+ },
667
+ {
668
+ title: {
669
+ label: '可清除',
670
+ tip: '开启后选择框右侧显示清除图标,点击可清空已选值。',
671
+ },
672
+ name: '!hasClear',
673
+ condition: (target: any) => {
674
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
675
+ return ['Select', 'DatePicker', 'DateTimePicker'].includes(t)
676
+ },
677
+ extraProps: {
678
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.hasClear,
679
+ setValue: (target: any, value: boolean) => {
680
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
681
+ cp.hasClear = value
682
+ target.parent?.setPropValue?.('componentProps', cp)
683
+ },
684
+ },
685
+ setter: { componentName: 'BoolSetter', initialValue: false },
686
+ },
687
+ {
688
+ title: {
689
+ label: '多选模式',
690
+ tip: '设置下拉框的选择模式:\n· 单选(single):只能选一个选项(默认)\n· 多选(multiple):可以选多个,以标签形式展示\n· 标签输入(tag):可以选多个,也支持直接输入新标签',
691
+ },
692
+ name: '!mode',
693
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'Select',
694
+ extraProps: {
695
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.mode || 'single',
696
+ setValue: (target: any, value: string) => {
697
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
698
+ cp.mode = value
699
+ target.parent?.setPropValue?.('componentProps', cp)
700
+ },
701
+ },
702
+ setter: {
703
+ componentName: 'RadioGroupSetter',
704
+ props: {
705
+ options: [
706
+ { title: '单选', value: 'single' },
707
+ { title: '多选', value: 'multiple' },
708
+ { title: '标签', value: 'tag' },
709
+ ],
710
+ },
711
+ initialValue: 'single',
712
+ },
713
+ },
714
+ // ── DatePicker / DateTimePicker:日期格式
715
+ {
716
+ title: {
717
+ label: '日期格式',
718
+ tip: '日期显示和提交的格式字符串。\n常用:\n· 仅日期:YYYY-MM-DD(默认)\n· 日期+时间:YYYY-MM-DD HH:mm\n· 年月:YYYY-MM',
719
+ },
720
+ name: '!dateFormat',
721
+ condition: (target: any) => {
722
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
723
+ return ['DatePicker', 'DateTimePicker'].includes(t)
724
+ },
725
+ extraProps: {
726
+ getValue: (target: any) => {
727
+ const t = target.parent?.getPropValue?.('componentType')
728
+ return target.parent?.getPropValue?.('componentProps')?.format || (t === 'DateTimePicker' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')
729
+ },
730
+ setValue: (target: any, value: string) => {
731
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
732
+ cp.format = value
733
+ target.parent?.setPropValue?.('componentProps', cp)
734
+ },
735
+ },
736
+ setter: { componentName: 'StringSetter', initialValue: 'YYYY-MM-DD' },
737
+ },
738
+ // ── Upload:文件类型 / 数量限制
739
+ {
740
+ title: {
741
+ label: '允许的文件类型',
742
+ tip: '限制可上传的文件类型(传给 input[accept])。\n例如:image/* 只允许图片,.pdf 只允许 PDF,不填则不限制。',
743
+ },
744
+ name: '!accept',
745
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'Upload',
746
+ extraProps: {
747
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.accept,
748
+ setValue: (target: any, value: string) => {
749
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
750
+ if (!value) delete cp.accept
751
+ else cp.accept = value
752
+ target.parent?.setPropValue?.('componentProps', cp)
753
+ },
754
+ },
755
+ setter: { componentName: 'StringSetter' },
756
+ },
757
+ {
758
+ title: {
759
+ label: '最多上传数量',
760
+ tip: '允许上传的最多文件数量,超出后禁止继续上传。不填则不限制。',
761
+ },
762
+ name: '!limit',
763
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'Upload',
764
+ extraProps: {
765
+ getValue: (target: any) => target.parent?.getPropValue?.('componentProps')?.limit,
766
+ setValue: (target: any, value: number) => {
767
+ const cp = { ...(target.parent?.getPropValue?.('componentProps') || {}) }
768
+ if (value === undefined || value === null || (value as any) === '') delete cp.limit
769
+ else cp.limit = value
770
+ target.parent?.setPropValue?.('componentProps', cp)
771
+ },
772
+ },
773
+ setter: { componentName: 'NumberSetter', props: { min: 1 } },
774
+ },
775
+ // ── 表单项控制
776
+ {
777
+ title: {
778
+ label: '自动校验',
779
+ tip: '开启后每次字段值变化时自动触发校验,不需等到提交才验证。\n适合需要即时反馈的场景(如手机号格式)。',
780
+ },
781
+ name: '!autoValidate',
782
+ extraProps: {
783
+ getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.autoValidate,
784
+ setValue: (target: any, value: boolean) => {
785
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
786
+ fp.autoValidate = value
787
+ target.parent?.setPropValue?.('formItemProps', fp)
788
+ },
789
+ },
790
+ setter: { componentName: 'BoolSetter', initialValue: false },
791
+ },
792
+ {
793
+ title: {
794
+ label: '隐藏冒号',
795
+ tip: '开启后在标题文字后面不显示冒号。',
796
+ },
797
+ name: '!colon',
798
+ extraProps: {
799
+ getValue: (target: any) => {
800
+ const v = target.parent?.getPropValue?.('formItemProps')?.colon
801
+ return v === false
802
+ },
803
+ setValue: (target: any, value: boolean) => {
804
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
805
+ fp.colon = !value
806
+ target.parent?.setPropValue?.('formItemProps', fp)
807
+ },
808
+ },
809
+ setter: { componentName: 'BoolSetter', initialValue: false },
810
+ },
811
+ // ── 表单校验
812
+ // 最小长度(Input / TextArea / CheckboxGroup)
813
+ {
814
+ title: {
815
+ label: '最少字符数',
816
+ tip: 'Input/TextArea:输入字符不能少于此数量。\nCheckboxGroup:至少要选几项。',
817
+ },
818
+ name: '!minLength',
819
+ condition: (target: any) => {
820
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
821
+ return ['Input', 'TextArea', 'CheckboxGroup'].includes(t)
822
+ },
823
+ extraProps: {
824
+ getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.minLength,
825
+ setValue: (target: any, value: number) => {
826
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
827
+ if (value === undefined || value === null || (value as any) === '') delete fp.minLength
828
+ else fp.minLength = value
829
+ target.parent?.setPropValue?.('formItemProps', fp)
830
+ },
831
+ },
832
+ setter: { componentName: 'NumberSetter', props: { min: 0 } },
833
+ },
834
+ // 最大长度(Input / TextArea / CheckboxGroup / Select multiple)
835
+ {
836
+ title: {
837
+ label: '最多字符数(校验)',
838
+ tip: 'Input/TextArea:输入字符不能超过此数量(校验维度,与组件层的最大字符数独立)。\nCheckboxGroup:最多能选几项。',
839
+ },
840
+ name: '!maxLengthValidate',
841
+ condition: (target: any) => {
842
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
843
+ return ['Input', 'TextArea', 'CheckboxGroup'].includes(t)
844
+ },
845
+ extraProps: {
846
+ getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.maxLength,
847
+ setValue: (target: any, value: number) => {
848
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
849
+ if (value === undefined || value === null || (value as any) === '') delete fp.maxLength
850
+ else fp.maxLength = value
851
+ target.parent?.setPropValue?.('formItemProps', fp)
852
+ },
853
+ },
854
+ setter: { componentName: 'NumberSetter', props: { min: 0 } },
855
+ },
856
+ // 最小/最大值(NumberPicker)
857
+ {
858
+ title: {
859
+ label: '最小数值(校验)',
860
+ tip: '数字字段的最小合法值(校验维度)。',
861
+ },
862
+ name: '!minValue',
863
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker',
864
+ extraProps: {
865
+ getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.min,
866
+ setValue: (target: any, value: number) => {
867
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
868
+ if (value === undefined || value === null || (value as any) === '') delete fp.min
869
+ else fp.min = value
870
+ target.parent?.setPropValue?.('formItemProps', fp)
871
+ },
872
+ },
873
+ setter: { componentName: 'NumberSetter' },
874
+ },
875
+ {
876
+ title: {
877
+ label: '最大数值(校验)',
878
+ tip: '数字字段的最大合法值(校验维度)。',
879
+ },
880
+ name: '!maxValue',
881
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'NumberPicker',
882
+ extraProps: {
883
+ getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.max,
884
+ setValue: (target: any, value: number) => {
885
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
886
+ if (value === undefined || value === null || (value as any) === '') delete fp.max
887
+ else fp.max = value
888
+ target.parent?.setPropValue?.('formItemProps', fp)
889
+ },
890
+ },
891
+ setter: { componentName: 'NumberSetter' },
892
+ },
893
+ // 正则校验(Input / TextArea)—— 提供预置规则 + 自定义
894
+ {
895
+ title: {
896
+ label: '正则校验',
897
+ tip: '选择常用的格式校验规则,或选"自定义"后填写自己的正则表达式(无需前后加斜杠)。\n常用示例:\n· 手机号:^1[3-9]\\d{9}$\n· 邮箱:^[\\w.]+@[\\w.]+\\.[a-z]{2,}$\n· 正整数:^\\d+$\n· 身份证号:^\\d{17}[\\dXx]$',
898
+ },
899
+ name: '!patternPreset',
900
+ condition: (target: any) => {
901
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
902
+ return ['Input', 'TextArea'].includes(t)
903
+ },
904
+ extraProps: {
905
+ getValue: (target: any) => {
906
+ const pattern = target.parent?.getPropValue?.('formItemProps')?.pattern
907
+ const presets: Record<string, string> = {
908
+ '^1[3-9]\\d{9}$': 'phone',
909
+ '^[\\w.+-]+@[\\w-]+\\.[a-z]{2,}$': 'email',
910
+ '^\\d+$': 'integer',
911
+ '^\\d+(\\.\\d+)?$': 'number',
912
+ '^\\d{17}[\\dXx]$': 'idcard',
913
+ '^[\\u4e00-\\u9fa5]+$': 'chinese',
914
+ }
915
+ return pattern ? (presets[pattern] || '__custom__') : ''
916
+ },
917
+ setValue: (target: any, value: string) => {
918
+ const patternMap: Record<string, string> = {
919
+ phone: '^1[3-9]\\d{9}$',
920
+ email: '^[\\w.+-]+@[\\w-]+\\.[a-z]{2,}$',
921
+ integer: '^\\d+$',
922
+ number: '^\\d+(\\.\\d+)?$',
923
+ idcard: '^\\d{17}[\\dXx]$',
924
+ chinese: '^[\\u4e00-\\u9fa5]+$',
925
+ }
926
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
927
+ if (!value || value === '__none__') {
928
+ delete fp.pattern
929
+ } else if (value === '__custom__') {
930
+ // 自定义时不修改 pattern,等用户在下方输入框填写
931
+ } else {
932
+ fp.pattern = patternMap[value]
933
+ }
934
+ target.parent?.setPropValue?.('formItemProps', fp)
935
+ },
936
+ },
937
+ setter: {
938
+ componentName: 'SelectSetter',
939
+ props: {
940
+ options: [
941
+ { label: '不校验', value: '__none__' },
942
+ { label: '手机号(11位)', value: 'phone' },
943
+ { label: '邮箱地址', value: 'email' },
944
+ { label: '整数', value: 'integer' },
945
+ { label: '数字(含小数)', value: 'number' },
946
+ { label: '身份证号(15/18位)', value: 'idcard' },
947
+ { label: '纯中文', value: 'chinese' },
948
+ { label: '自定义正则…', value: '__custom__' },
949
+ ],
950
+ },
951
+ initialValue: '__none__',
952
+ },
953
+ },
954
+ {
955
+ title: {
956
+ label: '自定义正则',
957
+ tip: '填写自定义正则表达式(无需前后加斜杠),例如:^1[3-9]\\d{9}$',
958
+ },
959
+ name: '!pattern',
960
+ condition: (target: any) => {
961
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
962
+ if (!['Input', 'TextArea'].includes(t)) return false
963
+ const preset = target.parent?.getPropValue?.('formItemProps')?.__patternPreset
964
+ // 当预置选择了"自定义"时显示,或者 pattern 已有值但不匹配任何预置时也显示
965
+ const pattern = target.parent?.getPropValue?.('formItemProps')?.pattern
966
+ const builtinPatterns = ['^1[3-9]\\d{9}$', '^[\\w.+-]+@[\\w-]+\\.[a-z]{2,}$', '^\\d+$', '^\\d+(\\.\\d+)?$', '^\\d{17}[\\dXx]$', '^[\\u4e00-\\u9fa5]+$']
967
+ return preset === '__custom__' || (!!pattern && !builtinPatterns.includes(pattern))
968
+ },
969
+ extraProps: {
970
+ getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.pattern,
971
+ setValue: (target: any, value: string) => {
972
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
973
+ if (!value) delete fp.pattern
974
+ else fp.pattern = value
975
+ target.parent?.setPropValue?.('formItemProps', fp)
976
+ },
977
+ },
978
+ setter: { componentName: 'StringSetter' },
979
+ },
980
+ {
981
+ title: {
982
+ label: '正则校验失败提示',
983
+ tip: '正则不匹配时显示的错误提示文字。',
984
+ },
985
+ name: '!patternMessage',
986
+ condition: (target: any) => {
987
+ const t = target.parent?.getPropValue?.('componentType') || 'Input'
988
+ return ['Input', 'TextArea'].includes(t) && !!target.parent?.getPropValue?.('formItemProps')?.pattern
989
+ },
990
+ extraProps: {
991
+ getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.patternMessage,
992
+ setValue: (target: any, value: string) => {
993
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
994
+ if (!value) delete fp.patternMessage
995
+ else fp.patternMessage = value
996
+ target.parent?.setPropValue?.('formItemProps', fp)
997
+ },
998
+ },
999
+ setter: { componentName: 'StringSetter' },
1000
+ },
1001
+ // 格式校验(仅 Input)
1002
+ {
1003
+ title: {
1004
+ label: '格式校验',
1005
+ tip: '快速选择常用格式进行校验:\n· number:必须是数字\n· email:必须是邮箱格式\n· url:必须是网址格式\n· tel:必须是电话格式',
1006
+ },
1007
+ name: '!format',
1008
+ condition: (target: any) => target.parent?.getPropValue?.('componentType') === 'Input',
1009
+ extraProps: {
1010
+ getValue: (target: any) => target.parent?.getPropValue?.('formItemProps')?.format,
1011
+ setValue: (target: any, value: string) => {
1012
+ const fp = { ...(target.parent?.getPropValue?.('formItemProps') || {}) }
1013
+ if (!value) delete fp.format
1014
+ else fp.format = value
1015
+ target.parent?.setPropValue?.('formItemProps', fp)
1016
+ },
1017
+ },
1018
+ setter: {
1019
+ componentName: 'SelectSetter',
1020
+ props: {
1021
+ options: [
1022
+ { label: '不限制', value: '' },
1023
+ { label: '数字 (number)', value: 'number' },
1024
+ { label: '邮箱 (email)', value: 'email' },
1025
+ { label: '网址 (url)', value: 'url' },
1026
+ { label: '电话 (tel)', value: 'tel' },
1027
+ ],
1028
+ },
1029
+ initialValue: '',
185
1030
  },
186
1031
  },
187
1032
  ],
188
- extraSetter: {
189
- componentName: 'MixedSetter',
190
- isRequired: false,
191
- props: {},
192
- },
193
1033
  },
194
1034
  },
195
1035
  },
@@ -197,69 +1037,744 @@ const CustomFormMeta: IPublicTypeComponentMetadata = {
197
1037
  initialValue: [],
198
1038
  },
199
1039
  },
1040
+ // ─────────────────────────────────────────────────────────────────────
1041
+ // 字段初始化
1042
+ // ─────────────────────────────────────────────────────────────────────
200
1043
  {
201
- title: '显示提交按钮',
202
- name: 'showSubmit',
203
- setter: {
204
- componentName: 'BoolSetter',
205
- initialValue: true,
1044
+ title: {
1045
+ label: '字段初始化',
1046
+ tip: '批量配置表单字段的初始值。\n\n· 字段名:与「表单项」中的「字段名」保持一致,例如 guest_count、user_name\n· 值类型:\n 固定值 — 直接填写静态默认值(字符串/数字/布尔/JSON)\n 变量绑定 — 绑定页面 state 或接口返回的动态变量\n\n💡 设置单个字段默认值时,也可在「表单项」列表中直接配置「默认值」,更快捷。',
206
1047
  },
207
- },
208
- {
209
- title: '提交按钮文字',
210
- name: 'submitText',
211
- setter: {
212
- componentName: 'StringSetter',
213
- initialValue: '提交',
1048
+ name: 'initialValues',
1049
+ extraProps: {
1050
+ getValue: (target: any) => {
1051
+ const iv = target.getProps().getPropValue('initialValues')
1052
+ if (!iv) return []
1053
+ if (Array.isArray(iv)) {
1054
+ return iv
1055
+ .map((row: any) => {
1056
+ const field = typeof row?.field === 'string' ? row.field.trim() : ''
1057
+ if (!field) return null
1058
+ const valueType = row?.valueType === 'variable' ? 'variable' : 'fixed'
1059
+ return {
1060
+ field,
1061
+ valueType,
1062
+ value: row?.value,
1063
+ }
1064
+ })
1065
+ .filter(Boolean)
1066
+ }
1067
+ return Object.entries(iv as Record<string, any>).map(([field, value]) => ({
1068
+ field: String(field).trim(),
1069
+ valueType: 'fixed',
1070
+ value,
1071
+ })).filter((row) => row.field)
1072
+ },
1073
+ setValue: (target: any, rows: any[]) => {
1074
+ if (!Array.isArray(rows)) return
1075
+ const cleanedRows = rows
1076
+ .map((row: any) => {
1077
+ const field = typeof row?.field === 'string' ? row.field.trim() : ''
1078
+ if (!field) return null
1079
+ const valueType = row?.valueType === 'variable' ? 'variable' : 'fixed'
1080
+ return {
1081
+ field,
1082
+ valueType,
1083
+ value: row?.value,
1084
+ }
1085
+ })
1086
+ .filter(Boolean)
1087
+ target.getProps().setPropValue('initialValues', cleanedRows)
1088
+ },
214
1089
  },
215
- },
216
- {
217
- title: '提交时校验',
218
- name: 'submitValidate',
219
1090
  setter: {
220
- componentName: 'BoolSetter',
221
- initialValue: true,
1091
+ componentName: 'ArraySetter',
1092
+ props: {
1093
+ itemSetter: {
1094
+ componentName: 'ObjectSetter',
1095
+ props: {
1096
+ config: {
1097
+ items: [
1098
+ {
1099
+ title: {
1100
+ label: '字段名',
1101
+ tip: '与「表单项」中的「字段名」保持一致,例如:guest_count、user_name',
1102
+ },
1103
+ name: 'field',
1104
+ important: true,
1105
+ display: 'inline',
1106
+ setter: {
1107
+ componentName: 'StringSetter',
1108
+ isRequired: true,
1109
+ initialValue: '',
1110
+ },
1111
+ },
1112
+ {
1113
+ title: {
1114
+ label: '值类型',
1115
+ tip: '· 固定值:静态初始值,例如"待处理"、10、true\n· 变量绑定:绑定页面 state 或动态数据,表单打开时从变量取值',
1116
+ },
1117
+ name: 'valueType',
1118
+ setter: {
1119
+ componentName: 'RadioGroupSetter',
1120
+ props: {
1121
+ options: [
1122
+ { title: '固定值', value: 'fixed' },
1123
+ { title: '变量绑定', value: 'variable' },
1124
+ ],
1125
+ },
1126
+ initialValue: 'fixed',
1127
+ },
1128
+ },
1129
+ {
1130
+ title: {
1131
+ label: '初始值',
1132
+ tip: '该字段的默认值。\n· 文本类填字符串,例如:待处理\n· 数字类填数字,例如:10\n· 下拉/单选填选项的 value 值,例如:male\n· 复选框填 JSON 数组,例如:["a","b"]',
1133
+ },
1134
+ name: 'value',
1135
+ condition: (target: any) =>
1136
+ (target.parent?.getPropValue?.('valueType') || 'fixed') === 'fixed',
1137
+ setter: {
1138
+ componentName: 'MixedSetter',
1139
+ props: {
1140
+ setters: [
1141
+ { componentName: 'StringSetter', title: '字符串' },
1142
+ { componentName: 'NumberSetter', title: '数字' },
1143
+ { componentName: 'BoolSetter', title: '布尔' },
1144
+ { componentName: 'JsonSetter', title: 'JSON(数组/对象)' },
1145
+ ],
1146
+ },
1147
+ },
1148
+ },
1149
+ {
1150
+ title: {
1151
+ label: '变量绑定',
1152
+ tip: '绑定页面 state 或外部传入的变量,表单打开时从该变量读取初始值。\n例如:绑定 state.selectedPackage 可将上一步选择的套餐自动填入。',
1153
+ },
1154
+ name: 'value',
1155
+ condition: (target: any) =>
1156
+ target.parent?.getPropValue?.('valueType') === 'variable',
1157
+ setter: {
1158
+ componentName: 'VariableSetter',
1159
+ },
1160
+ extraProps: {
1161
+ supportVariable: true,
1162
+ },
1163
+ },
1164
+ ],
1165
+ },
1166
+ },
1167
+ initialValue: () => ({ field: '', valueType: 'fixed', value: '' }),
1168
+ },
1169
+ },
1170
+ initialValue: [],
222
1171
  },
223
1172
  },
1173
+ // ─────────────────────────────────────────────────────────────────────
1174
+ // 计算字段
1175
+ // ─────────────────────────────────────────────────────────────────────
224
1176
  {
225
- title: '提交数据源',
226
- name: 'submitDataSource',
1177
+ title: {
1178
+ label: '计算字段',
1179
+ tip: '声明由多个表单字段自动拼接/计算出的衍生字段,每行一条规则。\n\n工作原理:每次任意字段值变化时自动重新计算,结果写入目标字段,提交时自动携带。\n\n表达式写法:用 {字段名} 引用表单中其他字段的当前值\n 例如:{package_name},{package_price}元,预约日期:{banquet_date}\n\n配置步骤:\n ① 目标字段名 = 要写入的字段名,例如:order_desc\n ② 表达式 = 输入模板文字,用 {字段名} 插入字段值\n ③ 保存后切换套餐/填值即可看到目标字段自动更新\n\n无需编写任何 JS 逻辑,配置即生效。',
1180
+ },
1181
+ name: 'computedFields',
1182
+ extraProps: {
1183
+ getValue: (target: any) => {
1184
+ const cf = target.getProps().getPropValue('computedFields')
1185
+ if (!cf) return []
1186
+ if (Array.isArray(cf)) return cf
1187
+ return Object.entries(cf as Record<string, string>).map(([targetField, template]) => ({ targetField, template }))
1188
+ },
1189
+ setValue: (target: any, value: any[]) => {
1190
+ if (!Array.isArray(value)) return
1191
+ target.getProps().setPropValue('computedFields', value.filter(Boolean))
1192
+ },
1193
+ },
227
1194
  setter: {
228
- componentName: 'SetterFormVariable',
1195
+ componentName: 'ArraySetter',
229
1196
  props: {
230
- showAttributesPanel: false,
1197
+ itemSetter: {
1198
+ componentName: 'ObjectSetter',
1199
+ props: {
1200
+ config: {
1201
+ items: [
1202
+ {
1203
+ title: {
1204
+ label: '目标字段名',
1205
+ tip: '计算结果写入的字段名,提交时携带此字段。\n建议取语义化名称,例如:order_desc、booking_summary\n该字段不需要出现在上方「表单项」中,提交时会自动追加到表单值里。',
1206
+ },
1207
+ name: 'targetField',
1208
+ important: true,
1209
+ display: 'inline',
1210
+ setter: { componentName: 'StringSetter', isRequired: true, initialValue: '' },
1211
+ },
1212
+ {
1213
+ title: {
1214
+ label: '表达式',
1215
+ tip: '用 {字段名} 引用表单字段的当前值,支持任意文字拼接。\n\n示例:{package_name},{guest_count}人,预订日期:{banquet_date}\n\n操作:先查看「表单项」中的字段名,在此处输入拼接模板,用 {字段名} 插入对应字段值,固定文字直接输入。',
1216
+ },
1217
+ name: 'template',
1218
+ setter: { componentName: 'StringSetter', isRequired: true, initialValue: '' },
1219
+ },
1220
+ ],
1221
+ },
1222
+ },
1223
+ initialValue: () => ({ targetField: '', template: '' }),
1224
+ },
231
1225
  },
1226
+ initialValue: [],
232
1227
  },
233
1228
  },
1229
+ // ─────────────────────────────────────────────────────────────────────
1230
+ // 字段联动
1231
+ // ─────────────────────────────────────────────────────────────────────
234
1232
  {
235
- title: '显示重置按钮',
236
- name: 'showReset',
1233
+ title: {
1234
+ label: '字段联动',
1235
+ tip: '配置"某字段变化 → 自动回填其他字段"的联动规则。\n\n【简单模式】适用于:选某个值 → 直接回填固定值或绑定变量\n 例如:选"豪华套餐" → 回填价格=9800\n · 监听字段:值发生变化时触发检查,填字段名,例如 package_name\n · 触发值:留空=任何变化都触发;填具体值=精确匹配才触发\n · 回填字段:要被自动填入值的目标字段名\n · 回填方式:固定值 或 变量(从 state 中选取)\n\n【数据源模式】适用于:根据所选值从一个列表中查找匹配项,自动回填该项的多个字段\n 典型场景:下拉框绑定了 state.packages(含 name/price/count),选择套餐名后自动回填价格和人数\n 监听字段 = package_name\n 数据源 = state.packages(与 package_name 下拉框的动态选项相同)\n 匹配字段 = value(Select 存储的是选项的 value 字段)\n 回填映射 = {"package_price":"price","guest_count":"count"}(JSON 格式)\n 注意:数据源中的每个对象需要有 matchKey 字段(用于匹配监听字段的值)以及各回填字段对应的属性。',
1236
+ },
1237
+ name: 'fieldLinkage',
237
1238
  setter: {
238
- componentName: 'BoolSetter',
239
- initialValue: false,
1239
+ componentName: 'ArraySetter',
1240
+ props: {
1241
+ itemSetter: {
1242
+ componentName: 'ObjectSetter',
1243
+ props: {
1244
+ config: {
1245
+ items: [
1246
+ // ── 通用:监听字段 ──────────────────────────────────────
1247
+ {
1248
+ title: {
1249
+ label: '监听字段',
1250
+ tip: '当此字段值变化时触发联动检查,填表单项的字段名(field),例如:package_name\n可从上方「表单项」列表中查看各字段名。',
1251
+ },
1252
+ name: 'watchField',
1253
+ important: true,
1254
+ display: 'inline',
1255
+ setter: { componentName: 'StringSetter', isRequired: true, initialValue: '' },
1256
+ },
1257
+ // ── 模式切换 ────────────────────────────────────────────
1258
+ {
1259
+ title: {
1260
+ label: '联动模式',
1261
+ tip: '· 简单:监听字段变化 → 直接设置某个字段的值(固定值或变量)\n· 数据源:监听字段变化 → 在指定数据源中查找匹配项 → 批量回填多个字段\n (适合"选套餐 → 联动回填价格、人数"等场景)',
1262
+ },
1263
+ name: 'linkageMode',
1264
+ setter: {
1265
+ componentName: 'RadioGroupSetter',
1266
+ props: {
1267
+ options: [
1268
+ { title: '简单', value: 'simple' },
1269
+ { title: '数据源', value: 'dataSource' },
1270
+ ],
1271
+ },
1272
+ initialValue: 'simple',
1273
+ },
1274
+ },
1275
+ // ── 简单模式:触发值 ────────────────────────────────────
1276
+ {
1277
+ title: {
1278
+ label: '触发值',
1279
+ tip: '监听字段的值等于此值时才触发回填。\n· 留空 = 该字段任何变化都触发\n· 填具体值 = 精确匹配后触发,例如:豪华套餐、1、true\n\n多个触发值:添加多条相同监听字段的规则,每条配置不同触发值。',
1280
+ },
1281
+ name: 'matchValue',
1282
+ condition: (target: any) =>
1283
+ (target.parent?.getPropValue?.('linkageMode') || 'simple') === 'simple',
1284
+ setter: { componentName: 'StringSetter', initialValue: '' },
1285
+ },
1286
+ // ── 简单模式:回填字段 ──────────────────────────────────
1287
+ {
1288
+ title: {
1289
+ label: '回填字段',
1290
+ tip: '触发条件满足后,自动填入值的目标字段名(field),例如:package_price\n可从上方「表单项」列表中查看各字段名。',
1291
+ },
1292
+ name: 'fillField',
1293
+ condition: (target: any) =>
1294
+ (target.parent?.getPropValue?.('linkageMode') || 'simple') === 'simple',
1295
+ setter: { componentName: 'StringSetter', initialValue: '' },
1296
+ },
1297
+ // ── 简单模式:回填方式 ──────────────────────────────────
1298
+ {
1299
+ title: {
1300
+ label: '回填方式',
1301
+ tip: '· 固定值:直接填写静态回填值,例如 9800、"待处理"\n· 变量:回填值来自页面 state(适合动态数据场景)',
1302
+ },
1303
+ name: 'fillValueType',
1304
+ condition: (target: any) =>
1305
+ (target.parent?.getPropValue?.('linkageMode') || 'simple') === 'simple',
1306
+ setter: {
1307
+ componentName: 'RadioGroupSetter',
1308
+ props: {
1309
+ options: [
1310
+ { title: '固定值', value: 'fixed' },
1311
+ { title: '变量', value: 'variable' },
1312
+ ],
1313
+ },
1314
+ initialValue: 'fixed',
1315
+ },
1316
+ },
1317
+ // ── 简单模式:回填值(固定)────────────────────────────
1318
+ {
1319
+ title: {
1320
+ label: '回填值',
1321
+ tip: '触发条件满足后填入「回填字段」的具体值。\n· 字符串示例:豪华套餐、待处理\n· 数字示例:9800、10\n如需动态值,请将「回填方式」切换为「变量」。',
1322
+ },
1323
+ name: 'fillValue',
1324
+ condition: (target: any) =>
1325
+ (target.parent?.getPropValue?.('linkageMode') || 'simple') === 'simple' &&
1326
+ (target.parent?.getPropValue?.('fillValueType') || 'fixed') === 'fixed',
1327
+ setter: {
1328
+ componentName: 'MixedSetter',
1329
+ props: {
1330
+ setters: [
1331
+ { componentName: 'StringSetter', title: '字符串' },
1332
+ { componentName: 'NumberSetter', title: '数字' },
1333
+ { componentName: 'BoolSetter', title: '布尔' },
1334
+ ],
1335
+ },
1336
+ },
1337
+ },
1338
+ // ── 简单模式:回填值(变量)────────────────────────────
1339
+ {
1340
+ title: {
1341
+ label: '回填变量',
1342
+ tip: '从页面 state 中选取变量作为回填值,表单联动时实时取该变量的当前值填入目标字段。\n\n如果 state 中没有现成变量,但回填值需要来自某个下拉框选项对象的字段(如选套餐后取价格),请改用「数据源」模式。',
1343
+ },
1344
+ name: 'fillValue',
1345
+ condition: (target: any) =>
1346
+ (target.parent?.getPropValue?.('linkageMode') || 'simple') === 'simple' &&
1347
+ target.parent?.getPropValue?.('fillValueType') === 'variable',
1348
+ setter: { componentName: 'VariableSetter' },
1349
+ extraProps: { supportVariable: true },
1350
+ },
1351
+ // ── 数据源模式:数据源 ──────────────────────────────────
1352
+ {
1353
+ title: {
1354
+ label: '数据源',
1355
+ tip: '从 state 中选取一个数组变量作为查找来源。\n\n典型配置:将 package_name 字段的「动态选项绑定」绑定到 state.packages,然后此处同样选择 state.packages。\n\n这样当 package_name 变化时,会在 state.packages 中找到匹配的项,再按「回填映射」将各字段值填入表单。',
1356
+ },
1357
+ name: 'dataSource',
1358
+ condition: (target: any) =>
1359
+ target.parent?.getPropValue?.('linkageMode') === 'dataSource',
1360
+ setter: { componentName: 'VariableSetter' },
1361
+ extraProps: { supportVariable: true },
1362
+ },
1363
+ // ── 数据源模式:匹配字段 ────────────────────────────────
1364
+ {
1365
+ title: {
1366
+ label: '匹配字段',
1367
+ tip: '在数据源的每个对象中,用哪个字段的值与监听字段的当前值进行比对。\n\n下拉框(Select)默认存储选项的 value 字段,此处填 value 即可。\n如果下拉框存储的是 label(名称),则填 label。\n\n示例:state.packages = [{value:"A", label:"豪华套餐", price:9800, count:10}, ...]\n则此处填 value(与 package_name 字段存储的选项 value 对应)。',
1368
+ },
1369
+ name: 'matchKey',
1370
+ condition: (target: any) =>
1371
+ target.parent?.getPropValue?.('linkageMode') === 'dataSource',
1372
+ setter: { componentName: 'StringSetter', initialValue: 'value' },
1373
+ },
1374
+ // ── 数据源模式:回填映射 ────────────────────────────────
1375
+ {
1376
+ title: {
1377
+ label: '回填映射',
1378
+ tip: '找到匹配项后,将数据源对象中的哪些字段值填入表单的哪些字段。\n\nJSON 格式:{ "表单字段名": "数据源字段名", ... }\n\n示例(state.packages 中每项有 price 和 count 属性):\n {"package_price":"price","guest_count":"count"}\n\n效果:匹配到套餐后,自动将 price 回填到 package_price 字段,count 回填到 guest_count 字段。',
1379
+ },
1380
+ name: 'fillFields',
1381
+ condition: (target: any) =>
1382
+ target.parent?.getPropValue?.('linkageMode') === 'dataSource',
1383
+ setter: {
1384
+ componentName: 'JsonSetter',
1385
+ initialValue: {},
1386
+ },
1387
+ },
1388
+ ],
1389
+ },
1390
+ },
1391
+ initialValue: () => ({ watchField: '', linkageMode: 'simple', matchValue: '', fillField: '', fillValueType: 'fixed', fillValue: '' }),
1392
+ },
1393
+ },
1394
+ initialValue: [],
240
1395
  },
241
1396
  },
1397
+ // ─────────────────────────────────────────────────────────────────────
1398
+ // 提交参数配置
1399
+ // ─────────────────────────────────────────────────────────────────────
242
1400
  {
243
- title: '重置按钮文字',
244
- name: 'resetText',
1401
+ title: {
1402
+ label: '提交参数配置',
1403
+ tip: '控制表单提交时如何将字段值构建为请求参数,支持三种模式:\n\n① 直接透传(passthrough,默认)\n字段名与接口参数名完全一致时无需配置——表单字段值直接作为请求 body,零配置即可工作。\n\n② 字段重命名(rename)\n表单字段名与接口参数名不一致时使用,按规则将指定字段重命名,其余字段仍按原名透传。\n例如:表单字段 user_name → 接口参数 username\n\n③ 动态表单格式(itemList)\n后端接口接收 itemList:[{columnName,columnValue},...] 格式时使用,适合 /dynamicFormTableRecord/ 等接口。\n例如:{user_name:"张三"} → [{columnName:"user_name",columnValue:"张三"}]',
1404
+ },
1405
+ name: 'submitMapping',
245
1406
  setter: {
246
- componentName: 'StringSetter',
247
- initialValue: '重置',
1407
+ componentName: 'ObjectSetter',
1408
+ props: {
1409
+ config: {
1410
+ items: [
1411
+ // ── 模式选择 ──────────────────────────────────────────────
1412
+ {
1413
+ title: {
1414
+ label: '参数构建模式',
1415
+ tip: '选择提交时参数的构建方式:\n· 直接透传:字段名与接口参数名一致,直接发送;最简单,首选。\n· 字段重命名:字段名与接口参数名不一致,在下方按需配置重命名规则即可。\n· 动态表单格式:接口接收 itemList:[{columnName,columnValue}] 格式,需完整配置字段映射列表。',
1416
+ },
1417
+ name: 'mode',
1418
+ setter: {
1419
+ componentName: 'SelectSetter',
1420
+ props: {
1421
+ options: [
1422
+ { label: '① 直接透传(字段名与接口参数名一致)', value: 'passthrough' },
1423
+ { label: '② 字段重命名(字段名与接口参数名不一致)', value: 'rename' },
1424
+ { label: '③ 动态表单格式(itemList 格式)', value: 'itemList' },
1425
+ ],
1426
+ },
1427
+ initialValue: 'passthrough',
1428
+ },
1429
+ },
1430
+ // ── rename 模式:字段重命名映射 ───────────────────────────
1431
+ {
1432
+ title: {
1433
+ label: '字段重命名规则',
1434
+ tip: '每行配置一条重命名规则,将表单字段名映射到接口参数名。\n· 来源字段(表单字段名):填写表单项中 field 的值,例如 user_name\n· 目标参数名(接口参数名):接口实际接收的参数名,例如 username\n\n未在此处列出的字段,将按原名透传到请求参数中。\n不需要填写表单中所有字段,只填需要重命名的字段即可。',
1435
+ },
1436
+ name: 'fieldRenameMap',
1437
+ condition: (target: any) =>
1438
+ target.parent?.getPropValue?.('mode') === 'rename',
1439
+ setter: {
1440
+ componentName: 'ArraySetter',
1441
+ props: {
1442
+ itemSetter: {
1443
+ componentName: 'ObjectSetter',
1444
+ props: {
1445
+ config: {
1446
+ items: [
1447
+ {
1448
+ title: {
1449
+ label: '来源字段(表单字段名)',
1450
+ tip: '表单项中 field 的值,即当前使用的字段名,例如:user_name',
1451
+ },
1452
+ name: 'fromField',
1453
+ important: true,
1454
+ display: 'inline',
1455
+ setter: {
1456
+ componentName: 'StringSetter',
1457
+ isRequired: true,
1458
+ initialValue: '',
1459
+ },
1460
+ },
1461
+ {
1462
+ title: {
1463
+ label: '目标参数名(接口参数名)',
1464
+ tip: '接口实际接收的参数名,例如:username',
1465
+ },
1466
+ name: 'toField',
1467
+ important: true,
1468
+ display: 'inline',
1469
+ setter: {
1470
+ componentName: 'StringSetter',
1471
+ isRequired: true,
1472
+ initialValue: '',
1473
+ },
1474
+ },
1475
+ ],
1476
+ },
1477
+ },
1478
+ initialValue: () => ({ fromField: '', toField: '' }),
1479
+ },
1480
+ },
1481
+ initialValue: [],
1482
+ },
1483
+ },
1484
+ // ── itemList 模式:目标字段名 ──────────────────────────────
1485
+ {
1486
+ title: {
1487
+ label: '目标字段名(itemList 写入位置)',
1488
+ tip: '组装后的 [{columnName,columnValue},...] 数组写入 finalValues 的字段名,默认 itemList。\n需与数据源静态 options.params 中预设的字段名一致,提交时该值会被组装结果覆盖。\n通常保持默认值 itemList 即可。',
1489
+ },
1490
+ name: 'targetField',
1491
+ condition: (target: any) =>
1492
+ target.parent?.getPropValue?.('mode') === 'itemList',
1493
+ setter: {
1494
+ componentName: 'StringSetter',
1495
+ initialValue: 'itemList',
1496
+ },
1497
+ },
1498
+ // ── itemList 模式:字段映射列表 ────────────────────────────
1499
+ {
1500
+ title: {
1501
+ label: '字段映射列表',
1502
+ tip: '每行定义一条映射规则,决定哪些字段进入 itemList,以及每项的 columnName。\n\n· 接口字段名(columnName):dynamicFormTableRecord 表的字段标识,与后端 schema 一致。\n· 来源字段(field):从哪个表单字段取值,填写表单项的 field 名,例如 user_name。\n· 模板(template):用 {字段名} 引用并拼接多个表单字段,与 field 二选一。\n 例如:{banquet_date} {banquet_time} → "2026-03-12 18:00"(将日期和时间合并为一个值)\n\n来源字段与模板只需填一项,两者都填时 field 优先。\n未在此处列出的表单字段不会进入 itemList(也不会出现在请求参数里)。',
1503
+ },
1504
+ name: 'items',
1505
+ condition: (target: any) =>
1506
+ target.parent?.getPropValue?.('mode') === 'itemList',
1507
+ setter: {
1508
+ componentName: 'ArraySetter',
1509
+ props: {
1510
+ itemSetter: {
1511
+ componentName: 'ObjectSetter',
1512
+ props: {
1513
+ config: {
1514
+ items: [
1515
+ {
1516
+ title: {
1517
+ label: '接口字段名(columnName)',
1518
+ tip: 'dynamicFormTableRecord 中的字段标识,例如:user_name、banquet_time、guest_count',
1519
+ },
1520
+ name: 'columnName',
1521
+ important: true,
1522
+ display: 'inline',
1523
+ setter: {
1524
+ componentName: 'StringSetter',
1525
+ isRequired: true,
1526
+ initialValue: '',
1527
+ },
1528
+ },
1529
+ {
1530
+ title: {
1531
+ label: '来源字段',
1532
+ tip: '从哪个表单字段取值,填写表单项的 field 名,例如:user_name\n与「模板」二选一,只需填一项。',
1533
+ },
1534
+ name: 'field',
1535
+ setter: {
1536
+ componentName: 'StringSetter',
1537
+ initialValue: '',
1538
+ },
1539
+ },
1540
+ {
1541
+ title: {
1542
+ label: '模板',
1543
+ tip: '用 {字段名} 引用并拼接多个字段值,例如:{banquet_date} {banquet_time}\n与「来源字段」二选一,只需填一项。',
1544
+ },
1545
+ name: 'template',
1546
+ setter: {
1547
+ componentName: 'StringSetter',
1548
+ initialValue: '',
1549
+ },
1550
+ },
1551
+ ],
1552
+ },
1553
+ },
1554
+ initialValue: () => ({ columnName: '', field: '' }),
1555
+ },
1556
+ },
1557
+ initialValue: [],
1558
+ },
1559
+ },
1560
+ ],
1561
+ },
1562
+ },
248
1563
  },
249
1564
  },
1565
+ // ─────────────────────────────────────────────────────────────────────
1566
+ // 分组六:操作按钮
1567
+ // ─────────────────────────────────────────────────────────────────────
1568
+ {
1569
+ type: 'group',
1570
+ title: '操作按钮',
1571
+ name: 'actionGroup',
1572
+ display: 'accordion',
1573
+ items: [
1574
+ // ── 对齐方式 ──────────────────────────────────────────────────────
1575
+ {
1576
+ title: {
1577
+ label: '对齐方式',
1578
+ tip: '表单底部操作按钮区域的水平对齐方式。\n· 居左:按钮靠左排列\n· 居中:按钮居中排列(默认)\n· 居右:按钮靠右排列',
1579
+ },
1580
+ name: 'operationAlign',
1581
+ setter: {
1582
+ componentName: 'RadioGroupSetter',
1583
+ props: {
1584
+ options: [
1585
+ { title: '居左', value: 'left' },
1586
+ { title: '居中', value: 'center' },
1587
+ { title: '居右', value: 'right' },
1588
+ ],
1589
+ },
1590
+ initialValue: 'center',
1591
+ },
1592
+ },
1593
+ // ── 按钮列表 ──────────────────────────────────────────────────────
1594
+ {
1595
+ title: {
1596
+ label: '按钮列表',
1597
+ tip: '配置表单底部的操作按钮。\n\n默认包含「立即预订」(提交,primary 样式)和「重置」(重置,normal 样式)两个按钮,可按需增删改。\n\n字段说明:\n· 按钮文字:按钮显示的文字,例如:立即预订、保存草稿、取消\n· 操作类型:\n 提交表单 → 校验通过后收集字段值 → 调用「提交数据源(Fetch)」接口 → 触发 onSubmit 事件\n 重置表单 → 清空所有字段值,触发 onReset 事件\n 自定义 → 直接触发「点击事件」,不操作表单\n· 按钮样式:主要(primary 蓝色)/ 次要(secondary)/ 普通(normal 灰色)\n· 提交数据源(Fetch):仅「提交表单」类型生效。点击后弹出 Fetch 数据源配置面板,选择接口地址和请求参数,请求参数支持绑定到表单字段值,提交时自动携带表单数据发起请求。\n· 提交时校验:关闭后跳过必填校验直接提交(适合草稿保存)\n· 点击事件:仅「自定义」类型生效\n\n不配置任何按钮时表单不显示按钮区域。',
1598
+ },
1599
+ name: 'operations',
1600
+ setter: {
1601
+ componentName: 'ArraySetter',
1602
+ props: {
1603
+ itemSetter: {
1604
+ componentName: 'ObjectSetter',
1605
+ props: {
1606
+ config: {
1607
+ items: [
1608
+ // ── 按钮文字
1609
+ {
1610
+ title: {
1611
+ label: '按钮文字',
1612
+ tip: '按钮上显示的文字,例如:立即预订、保存草稿、取消',
1613
+ },
1614
+ name: 'text',
1615
+ important: true,
1616
+ display: 'inline',
1617
+ setter: {
1618
+ componentName: 'StringSetter',
1619
+ isRequired: true,
1620
+ initialValue: '立即预订',
1621
+ },
1622
+ },
1623
+ // ── 操作类型
1624
+ {
1625
+ title: {
1626
+ label: '操作类型',
1627
+ tip: '· 提交表单:校验 → 收集字段值 → 调用「提交数据源(Fetch)」 → 触发 onSubmit 事件\n· 重置表单:清空所有字段,触发 onReset 事件\n· 自定义:直接调用「点击事件」函数,不涉及表单操作',
1628
+ },
1629
+ name: 'action',
1630
+ important: true,
1631
+ display: 'inline',
1632
+ setter: {
1633
+ componentName: 'SelectSetter',
1634
+ props: {
1635
+ options: [
1636
+ { label: '提交表单', value: 'submit' },
1637
+ { label: '重置表单', value: 'reset' },
1638
+ { label: '自定义', value: 'custom' },
1639
+ ],
1640
+ },
1641
+ initialValue: 'submit',
1642
+ },
1643
+ extraProps: {
1644
+ setValue: (target: any, value: string) => {
1645
+ // 根据操作类型自动填充默认文字(仅当文字为空时)
1646
+ const textMap: Record<string, string> = {
1647
+ submit: '立即预订',
1648
+ reset: '重置',
1649
+ custom: '按钮',
1650
+ }
1651
+ if (!target.parent?.getPropValue?.('text')) {
1652
+ target.parent?.setPropValue?.('text', textMap[value] || '按钮')
1653
+ }
1654
+ // 根据操作类型自动设置默认样式
1655
+ if (!target.parent?.getPropValue?.('type')) {
1656
+ const typeMap: Record<string, string> = {
1657
+ submit: 'primary',
1658
+ reset: 'normal',
1659
+ custom: 'normal',
1660
+ }
1661
+ target.parent?.setPropValue?.('type', typeMap[value] || 'normal')
1662
+ }
1663
+ target.parent?.setPropValue?.('action', value)
1664
+ },
1665
+ },
1666
+ },
1667
+ // ── 按钮样式
1668
+ {
1669
+ title: {
1670
+ label: '按钮样式',
1671
+ tip: '· 主要(primary):蓝色高亮,适合提交等主操作\n· 次要(secondary):灰白色,适合次要操作\n· 普通(normal):普通灰色样式,适合重置等辅助操作',
1672
+ },
1673
+ name: 'type',
1674
+ setter: {
1675
+ componentName: 'SelectSetter',
1676
+ props: {
1677
+ options: [
1678
+ { label: '主要 (primary)', value: 'primary' },
1679
+ { label: '次要 (secondary)', value: 'secondary' },
1680
+ { label: '普通 (normal)', value: 'normal' },
1681
+ ],
1682
+ },
1683
+ initialValue: 'normal',
1684
+ },
1685
+ },
1686
+ // ── 提交时校验(仅 submit)
1687
+ {
1688
+ title: {
1689
+ label: '提交时校验',
1690
+ tip: '开启后点击此按钮时先执行必填校验,校验不通过则不触发提交。\n关闭后跳过校验,直接提交(适合"草稿保存"等场景)。\n仅对「提交表单」类型按钮生效。',
1691
+ },
1692
+ name: 'submitValidate',
1693
+ condition: (target: any) =>
1694
+ target.parent?.getPropValue?.('action') === 'submit',
1695
+ setter: {
1696
+ componentName: 'BoolSetter',
1697
+ initialValue: true,
1698
+ },
1699
+ },
1700
+ // ── 提交数据源(Fetch,仅 submit)
1701
+ {
1702
+ title: {
1703
+ label: '提交数据源(Fetch)',
1704
+ tip: '绑定页面级 Fetch 数据源,点击提交按钮校验通过后自动调用该数据源发起请求。\n\n【配置步骤】\n1. 在页面左侧「数据源」面板新建一个 fetch 类型数据源\n · 填写接口地址(URI)、请求方式(POST/PUT)、鉴权 Headers\n · isInit 设为 false(提交时才触发,不需要页面初始化时自动加载)\n2. 在此处点击「变量」按钮,选择 this.dataSourceMap.<数据源ID>\n\n【参数说明】\n表单提交时会将所有字段值(含 computedFields / submitMapping 处理后的 itemList)\n作为覆盖参数传入 .load(values),与数据源静态 params 合并,无需手写 onSubmit。\n\n【多按钮场景】\n每个「提交表单」按钮可绑定独立的数据源,如"保存草稿"绑定草稿接口,"正式提交"绑定提交接口。\n\n仅对「提交表单」类型按钮生效。',
1705
+ },
1706
+ name: 'dataSource',
1707
+ condition: (target: any) =>
1708
+ target.parent?.getPropValue?.('action') === 'submit',
1709
+ setter: {
1710
+ componentName: 'MixedSetter',
1711
+ props: {
1712
+ setters: [
1713
+ {
1714
+ componentName: 'VariableSetter',
1715
+ title: '绑定数据源变量',
1716
+ },
1717
+ {
1718
+ componentName: 'ExpressionSetter',
1719
+ title: '表达式',
1720
+ },
1721
+ ],
1722
+ },
1723
+ },
1724
+ },
1725
+ // ── 点击事件(仅 custom)
1726
+ {
1727
+ title: {
1728
+ label: '点击事件',
1729
+ tip: '自定义按钮被点击时调用的函数,例如:跳转页面、打开弹窗。\n仅对「自定义」类型按钮生效。',
1730
+ },
1731
+ name: 'onClick',
1732
+ condition: (target: any) =>
1733
+ target.parent?.getPropValue?.('action') === 'custom',
1734
+ setter: {
1735
+ componentName: 'FunctionSetter',
1736
+ },
1737
+ extraProps: {
1738
+ supportVariable: true,
1739
+ },
1740
+ },
1741
+ ],
1742
+ },
1743
+ },
1744
+ initialValue: () => ({ text: '立即预订', action: 'submit', type: 'primary', submitValidate: true }),
1745
+ },
1746
+ },
1747
+ initialValue: [
1748
+ { text: '立即预订', action: 'submit', type: 'primary', submitValidate: true },
1749
+ { text: '重置', action: 'reset', type: 'normal' },
1750
+ ],
1751
+ },
1752
+ },
1753
+ ],
1754
+ },
250
1755
  ],
251
1756
  supports: {
252
1757
  style: true,
253
1758
  events: [
1759
+ {
1760
+ name: 'onReset',
1761
+ template:
1762
+ "onReset(\${extParams}){\n// \u70b9\u51fb\u91cd\u7f6e\u6309\u94ae\u540e\u89e6\u53d1\n// \u53ef\u5728\u6b64\u5904\u540c\u6b65\u9875\u9762 state \u6216\u6267\u884c\u5176\u4ed6\u6e05\u7a7a\u903b\u8f91\nconsole.log('[CustomForm] onReset');\n}",
1763
+ },
1764
+ {
1765
+ name: 'onChange',
1766
+ template:
1767
+ "onChange(field, value, allValues,\${extParams}){\n// 任意字段值变化时触发\n// field = 变化的字段名(字符串)\n// value = 该字段的最新值\n// allValues = 当前所有字段值(含 computedFields 计算字段)\n// 常见用法:将字段值同步到页面 state\n// this.setState({ [field]: value })\nconsole.log('[CustomForm] onChange', field, value, allValues);\n}",
1768
+ },
254
1769
  {
255
1770
  name: 'onSubmit',
256
1771
  template:
257
- "onSubmit(values, errors, field,${extParams}){\n// 提交成功(无校验错误)\nconsole.log('onSubmit', values, errors, field);\n}",
1772
+ "onSubmit(values, errors, field,\${extParams}){\n// 提交成功(校验通过)时触发\n// values = 所有字段的当前值(含 computedFields 计算字段)\n// 无需再手动组装字段,直接使用 values 对象即可\nconsole.log('[CustomForm] onSubmit', values);\n}",
258
1773
  },
259
1774
  {
260
1775
  name: 'onSubmitFailed',
261
1776
  template:
262
- "onSubmitFailed(values, errors, field,${extParams}){\n// 提交失败(校验错误)\nconsole.log('onSubmitFailed', values, errors, field);\n}",
1777
+ "onSubmitFailed(values, errors, field,\${extParams}){\n// 提交失败(校验未通过)时触发\n// errors = 校验错误信息对象,key 为字段名\nconsole.log('[CustomForm] onSubmitFailed', errors);\n}",
263
1778
  },
264
1779
  ],
265
1780
  },
@@ -268,34 +1783,78 @@ const CustomFormMeta: IPublicTypeComponentMetadata = {
268
1783
 
269
1784
  const snippets: IPublicTypeSnippet[] = [
270
1785
  {
271
- title: '自定义表单',
1786
+ title: '自定义表单(完整示例)',
272
1787
  screenshot: '',
273
1788
  schema: {
274
1789
  componentName: 'CustomForm',
275
1790
  props: {
276
- columns: 1,
1791
+ columns: 2,
1792
+ initialValues: [
1793
+ { field: 'guest_count', valueType: 'fixed', value: 10 },
1794
+ ],
1795
+ computedFields: [
1796
+ { targetField: 'order_desc', template: '{package_name},{package_price}元,预订日期:{banquet_date},人数:{guest_count}人' },
1797
+ ],
277
1798
  formItems: [
278
1799
  {
279
- field: 'name',
280
- label: '姓名',
1800
+ field: 'banquet_date',
1801
+ label: '宴请日期',
281
1802
  required: true,
1803
+ placeholder: '请选择日期时间',
1804
+ componentType: 'DateTimePicker',
1805
+ componentProps: { format: 'YYYY-MM-DD HH:mm' },
1806
+ },
1807
+ {
1808
+ field: 'guest_count',
1809
+ label: '宴请人数',
1810
+ required: true,
1811
+ placeholder: '请输入人数',
1812
+ componentType: 'NumberPicker',
1813
+ },
1814
+ {
1815
+ field: 'package_name',
1816
+ label: '套餐名称',
1817
+ required: true,
1818
+ componentType: 'Select',
1819
+ placeholder: '请选择套餐',
1820
+ options: [],
1821
+ },
1822
+ {
1823
+ field: 'package_price',
1824
+ label: '套餐价格',
282
1825
  componentType: 'Input',
283
- componentProps: {
284
- placeholder: '请输入姓名',
285
- },
1826
+ componentProps: { readOnly: true },
1827
+ extra: '选择套餐后自动填入',
286
1828
  },
287
1829
  {
288
- field: 'gender',
289
- label: '性别',
290
- componentType: 'RadioGroup',
291
- options: [
292
- { label: '', value: 'male' },
293
- { label: '女', value: 'female' },
294
- ],
1830
+ field: 'user_name',
1831
+ label: '预订人姓名',
1832
+ required: true,
1833
+ placeholder: '请输入姓名',
1834
+ componentType: 'Input',
295
1835
  },
1836
+ {
1837
+ field: 'user_mobile',
1838
+ label: '联系方式',
1839
+ required: true,
1840
+ placeholder: '请输入手机号',
1841
+ componentType: 'Input',
1842
+ },
1843
+ {
1844
+ field: 'memo',
1845
+ label: '备注',
1846
+ placeholder: '请输入备注(选填)',
1847
+ componentType: 'TextArea',
1848
+ columnSpan: 2,
1849
+ },
1850
+ ],
1851
+ fieldLinkage: [
1852
+ { watchField: 'package_name', linkageMode: 'simple', matchValue: '豪华套餐', fillField: 'package_price', fillValueType: 'fixed', fillValue: 9800 },
1853
+ ],
1854
+ operations: [
1855
+ { text: '立即预订', action: 'submit', type: 'primary', submitValidate: true },
1856
+ { text: '重置', action: 'reset', type: 'normal' },
296
1857
  ],
297
- showSubmit: true,
298
- submitText: '提交',
299
1858
  },
300
1859
  },
301
1860
  },