openyida 2026.7.29-beta.2 → 2026.7.30

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.
package/README.md CHANGED
@@ -387,6 +387,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
387
387
  | Command | Description |
388
388
  |---------|-------------|
389
389
  | `openyida create-form create <appType> ... [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a form page |
390
+ | `openyida create-form validate-fields <fieldsJsonOrFile> [--json]` | Validate form field JSON locally |
390
391
  | `openyida create-form update <appType> ... [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Update a form page |
391
392
  | `openyida create-form patch <appType> <formUuid> <patchJsonOrFile> [--open\|--no-open]` | Update a form page |
392
393
  | `openyida create-form rule <appType> <formUuid> <rulesJsonOrFile> [--open\|--no-open]` | Update a form page |
@@ -115,6 +115,10 @@ function createParseArgs(dependencies) {
115
115
  options.force = true;
116
116
  args.splice(i, 1);
117
117
  i--;
118
+ } else if (args[i] === '--json') {
119
+ options.json = true;
120
+ args.splice(i, 1);
121
+ i--;
118
122
  }
119
123
  }
120
124
 
@@ -132,6 +136,21 @@ function createParseArgs(dependencies) {
132
136
 
133
137
  const mode = args[0];
134
138
 
139
+ if (mode === 'validate-fields') {
140
+ if (args.length === 2) {
141
+ return {
142
+ mode: 'validate-fields',
143
+ fieldsJsonOrFile: args[1],
144
+ ...options
145
+ };
146
+ }
147
+ usage(
148
+ 'openyida create-form validate-fields <fieldsJsonOrFile> --json',
149
+ 'openyida create-form validate-fields .cache/openyida/forms/fields.json --json'
150
+ );
151
+ throwCreateFormError('openyida create-form validate-fields <fieldsJsonOrFile> --json', 'CREATE_FORM_INVALID_ARGUMENTS');
152
+ }
153
+
135
154
  if (mode === 'create') {
136
155
  if (args.length < 4) {
137
156
  usage(t('create_form.usage_create'), t('create_form.example_create'));
@@ -16,6 +16,8 @@ function dispatchCreateFormCommand(parsedArgs, authContext, handlers) {
16
16
  return handlers.bindDataSource(...args);
17
17
  case 'add-option':
18
18
  return handlers.addOption(...args);
19
+ case 'validate-fields':
20
+ return handlers.validateFields(...args);
19
21
  case 'create':
20
22
  return handlers.create(...args);
21
23
  default:
@@ -81,6 +81,9 @@ const { deepMerge, splitJsonPointer } = require('./create-form/schema-patch');
81
81
  const { FORM_RULES_BLOCK_END, FORM_RULES_BLOCK_START } = require('./create-form/rule-builder');
82
82
  const { SMART_VALIDATION_BLOCK_END, SMART_VALIDATION_BLOCK_START } = require('./create-form/validation-builder');
83
83
  const { createFieldNormalizers } = require('./create-form/field-normalizers');
84
+ const {
85
+ validateFormFieldDefinitions: collectFormFieldValidationDiagnostics,
86
+ } = require('./form-field-validator');
84
87
 
85
88
  function parseOpenOption(inputArgs) {
86
89
  const openOption = parseBrowserOpenOption(inputArgs);
@@ -351,182 +354,134 @@ function throwInvalidFieldDefinition(message, code, field, fieldPath, extra) {
351
354
  );
352
355
  }
353
356
 
354
- function validatePlainFieldObject(field, fieldPath) {
355
- if (!field || typeof field !== 'object' || Array.isArray(field)) {
356
- throwInvalidFieldDefinition(
357
- '字段定义必须是对象: ' + fieldPath,
358
- 'CREATE_FORM_INVALID_FIELD_DEFINITION',
359
- field,
360
- fieldPath
361
- );
357
+ function getValueAtDiagnosticPath(root, diagnosticPath, rootPath) {
358
+ const normalizedRootPath = rootPath || 'fields';
359
+ if (!diagnosticPath || diagnosticPath.indexOf(normalizedRootPath) !== 0) {
360
+ return undefined;
362
361
  }
363
- }
364
-
365
- function validateContainerChildrenArray(field, fieldPath, componentName) {
366
- if (field.children === undefined || field.children === null) {
367
- return [];
362
+ const relativePath = diagnosticPath.slice(normalizedRootPath.length);
363
+ const tokens = [];
364
+ const pattern = /\[(\d+)\]|\.([A-Za-z_$][\w$]*)/g;
365
+ let match;
366
+ while ((match = pattern.exec(relativePath)) !== null) {
367
+ tokens.push(match[1] !== undefined ? Number(match[1]) : match[2]);
368
368
  }
369
- if (!Array.isArray(field.children)) {
370
- throwInvalidFieldDefinition(
371
- componentName + '.children 必须是数组: ' + fieldPath,
372
- 'CREATE_FORM_INVALID_CONTAINER_CHILDREN',
373
- field,
374
- fieldPath,
375
- { componentName }
376
- );
369
+ let current = root;
370
+ for (let index = 0; index < tokens.length; index++) {
371
+ if (current === undefined || current === null) {
372
+ return undefined;
373
+ }
374
+ current = current[tokens[index]];
377
375
  }
378
- return field.children;
376
+ return current;
379
377
  }
380
378
 
381
- function hasNonEmptyAssociationFormUuid(field) {
382
- const associationForm = field && field.associationForm;
383
- if (!associationForm || typeof associationForm !== 'object' || Array.isArray(associationForm)) {
384
- return false;
379
+ function findDiagnosticField(fields, diagnosticPath, rootPath) {
380
+ let currentPath = diagnosticPath || rootPath || 'fields';
381
+ while (currentPath && currentPath !== (rootPath || 'fields')) {
382
+ const value = getValueAtDiagnosticPath(fields, currentPath, rootPath);
383
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
384
+ return value;
385
+ }
386
+ const dotIndex = currentPath.lastIndexOf('.');
387
+ const bracketIndex = currentPath.lastIndexOf('[');
388
+ const cutIndex = Math.max(dotIndex, bracketIndex);
389
+ if (cutIndex <= 0) {
390
+ break;
391
+ }
392
+ currentPath = currentPath.slice(0, cutIndex);
385
393
  }
386
- return typeof associationForm.formUuid === 'string' && associationForm.formUuid.trim().length > 0;
394
+ return undefined;
387
395
  }
388
396
 
389
- function validateAssociationFormFieldDefinition(field, fieldPath) {
390
- if (hasNonEmptyAssociationFormUuid(field)) {
391
- return;
392
- }
393
- throwInvalidFieldDefinition(
394
- 'AssociationFormField.associationForm.formUuid 必须是非空字符串: ' + fieldPath,
395
- 'CREATE_FORM_ASSOCIATION_FORM_UUID_MISSING',
396
- field,
397
- fieldPath,
398
- { componentName: 'AssociationFormField' }
399
- );
397
+ function mapFieldValidationCode(code) {
398
+ const cliErrorCodesByValidationCode = {
399
+ FIELD_TYPE_MISSING: 'CREATE_FORM_FIELD_TYPE_MISSING',
400
+ INVALID_FIELD_DEFINITION: 'CREATE_FORM_INVALID_FIELD_DEFINITION',
401
+ INVALID_FIELDS_ROOT: 'CREATE_FORM_INVALID_FIELD_DEFINITION',
402
+ UNSUPPORTED_FIELD_TYPE: 'CREATE_FORM_UNSUPPORTED_FIELD_TYPE',
403
+ COLUMN_OUTSIDE_COLUMN_CONTAINER: 'CREATE_FORM_COLUMN_OUTSIDE_COLUMNS_LAYOUT',
404
+ ASSOCIATION_FORM_MISSING: 'CREATE_FORM_ASSOCIATION_FORM_UUID_MISSING',
405
+ TABLE_CHILD_PRESENTATION_UNSUPPORTED: 'CREATE_FORM_TABLE_CHILD_PRESENTATION_UNSUPPORTED',
406
+ INVALID_TABLE_FIELD_CHILDREN_DEPTH: 'CREATE_FORM_INVALID_TABLE_CHILDREN',
407
+ INVALID_COLUMN_CONTAINER_CHILDREN_DEPTH: 'CREATE_FORM_INVALID_COLUMN_CONTAINER_CHILDREN_DEPTH',
408
+ INVALID_CHILDREN_SHAPE: 'CREATE_FORM_INVALID_CONTAINER_CHILDREN',
409
+ NESTED_TABLE_FIELD_UNSUPPORTED: 'CREATE_FORM_NESTED_TABLE_FIELD_UNSUPPORTED',
410
+ OPTION_FIELD_DATASOURCE_MISSING: 'CREATE_FORM_OPTION_FIELD_DATASOURCE_MISSING',
411
+ BUSINESS_FIELD_LABEL_MISSING: 'CREATE_FORM_BUSINESS_FIELD_LABEL_MISSING',
412
+ INVALID_MULTIPLE_TYPE: 'CREATE_FORM_INVALID_MULTIPLE_TYPE',
413
+ INVALID_DIVIDER_TITLE: 'CREATE_FORM_INVALID_DIVIDER_TITLE',
414
+ INVALID_DIVIDER_TYPE: 'CREATE_FORM_INVALID_DIVIDER_TYPE',
415
+ };
416
+ return cliErrorCodesByValidationCode[code] || 'CREATE_FORM_FIELD_JSON_INVALID';
400
417
  }
401
418
 
402
- function validateFieldDefinition(field, fieldPath, options) {
403
- const validationOptions = options || {};
404
- validatePlainFieldObject(field, fieldPath);
405
-
406
- const componentName = normalizeFormDefinitionType(field);
407
- if (!componentName) {
408
- throwInvalidFieldDefinition(
409
- '字段定义缺少 type/componentName/componentType: ' + fieldPath,
410
- 'CREATE_FORM_FIELD_TYPE_MISSING',
411
- field,
412
- fieldPath
413
- );
419
+ function normalizeDiagnosticDetailPath(diagnostic) {
420
+ if (!diagnostic || !diagnostic.path) {
421
+ return 'fields';
414
422
  }
415
-
416
- if (isFormPresentationComponent(componentName)) {
417
- if (validationOptions.disallowPresentation) {
418
- throwInvalidFieldDefinition(
419
- 'TableField.children 不支持展示布局组件: ' + componentName,
420
- 'CREATE_FORM_TABLE_CHILD_PRESENTATION_UNSUPPORTED',
421
- field,
422
- fieldPath,
423
- { componentName }
424
- );
425
- }
426
- if (componentName === 'Column' && !validationOptions.allowColumn) {
427
- throwInvalidFieldDefinition(
428
- 'Column 只能作为 ColumnContainer.children 的 Column 对象使用: ' + fieldPath,
429
- 'CREATE_FORM_COLUMN_OUTSIDE_COLUMNS_LAYOUT',
430
- field,
431
- fieldPath,
432
- { componentName }
433
- );
434
- }
435
- if (componentName === 'ColumnsLayout') {
436
- validateColumnsLayoutDefinition(field, fieldPath);
437
- } else if (componentName === 'PageSection') {
438
- validateContainerChildrenArray(field, fieldPath, componentName).forEach(function (child, childIndex) {
439
- validateFieldDefinition(child, fieldPath + '.children[' + childIndex + ']');
440
- });
441
- } else if (componentName === 'Column') {
442
- validateContainerChildrenArray(field, fieldPath, componentName).forEach(function (child, childIndex) {
443
- validateFieldDefinition(child, fieldPath + '.children[' + childIndex + ']');
444
- });
445
- }
446
- return componentName;
423
+ if (diagnostic.code === 'FIELD_TYPE_MISSING' && diagnostic.path.endsWith('.type')) {
424
+ return diagnostic.path.slice(0, -'.type'.length);
447
425
  }
448
-
449
- if (!isSupportedBusinessFieldType(componentName)) {
450
- throwInvalidFieldDefinition(
451
- '不支持的字段类型: ' + componentName,
452
- 'CREATE_FORM_UNSUPPORTED_FIELD_TYPE',
453
- field,
454
- fieldPath,
455
- { componentName }
456
- );
426
+ if (diagnostic.code === 'ASSOCIATION_FORM_MISSING' && diagnostic.path.endsWith('.associationForm')) {
427
+ return diagnostic.path.slice(0, -'.associationForm'.length);
457
428
  }
429
+ return diagnostic.path;
430
+ }
458
431
 
459
- if (componentName === 'AssociationFormField') {
460
- validateAssociationFormFieldDefinition(field, fieldPath);
461
- }
432
+ function validateFormFieldDefinitions(fields, rootPath) {
433
+ const diagnostics = collectFormFieldValidationDiagnostics(fields, {
434
+ rootPath: rootPath || 'fields',
435
+ });
436
+ throwFieldValidationDiagnostics(fields, diagnostics, rootPath || 'fields');
437
+ }
462
438
 
463
- if (componentName === 'TableField') {
464
- validateTableFieldChildren(field, fieldPath, validationOptions);
439
+ function throwFieldValidationDiagnostics(fields, diagnostics, rootPath) {
440
+ if (diagnostics.length === 0) {
441
+ return;
465
442
  }
466
443
 
467
- return componentName;
444
+ const first = diagnostics[0];
445
+ const detailPath = normalizeDiagnosticDetailPath(first);
446
+ const field = findDiagnosticField(fields, detailPath, rootPath || 'fields');
447
+ const details = buildInvalidFieldDefinitionDetails(field, detailPath, {
448
+ diagnostics,
449
+ expected: first.expected,
450
+ actual: first.actual,
451
+ suggestion: first.suggestion,
452
+ });
453
+ throwCreateFormError(
454
+ first.message,
455
+ mapFieldValidationCode(first.code),
456
+ details
457
+ );
468
458
  }
469
459
 
470
- function validateColumnsLayoutDefinition(field, fieldPath) {
471
- const columns = validateContainerChildrenArray(field, fieldPath, 'ColumnsLayout');
472
- columns.forEach(function (columnDefinition, columnIndex) {
473
- const columnPath = fieldPath + '.children[' + columnIndex + ']';
474
- if (Array.isArray(columnDefinition)) {
475
- columnDefinition.forEach(function (child, childIndex) {
476
- validateFieldDefinition(child, columnPath + '[' + childIndex + ']');
477
- });
478
- return;
479
- }
480
- if (
481
- columnDefinition &&
482
- typeof columnDefinition === 'object' &&
483
- !Array.isArray(columnDefinition) &&
484
- normalizeFormDefinitionType(columnDefinition) === 'Column'
485
- ) {
486
- validateFieldDefinition(columnDefinition, columnPath, { allowColumn: true });
487
- return;
488
- }
489
- throwInvalidFieldDefinition(
490
- 'ColumnContainer.children 只能包含二维字段数组或 Column 对象: ' + columnPath,
491
- 'CREATE_FORM_INVALID_COLUMNS_LAYOUT_CHILDREN',
492
- columnDefinition,
493
- columnPath,
494
- { componentName: normalizeFormDefinitionType(columnDefinition) || readFormDefinitionType(columnDefinition) }
495
- );
460
+ function collectSingleFieldValidationDiagnostics(field, rootPath) {
461
+ const internalRootPath = '__field';
462
+ const diagnostics = collectFormFieldValidationDiagnostics([field], {
463
+ rootPath: internalRootPath,
464
+ });
465
+ return diagnostics.map(function (diagnostic) {
466
+ return Object.assign({}, diagnostic, {
467
+ path: diagnostic.path.replace(internalRootPath + '[0]', rootPath),
468
+ });
496
469
  });
497
470
  }
498
471
 
499
- function validateTableFieldChildren(field, fieldPath, options) {
500
- if (field.children === undefined || field.children === null) {
501
- return;
502
- }
503
- if (!Array.isArray(field.children)) {
504
- throwInvalidFieldDefinition(
505
- 'TableField.children 必须是字段数组: ' + fieldPath,
506
- 'CREATE_FORM_INVALID_TABLE_CHILDREN',
507
- field,
508
- fieldPath,
509
- { componentName: 'TableField' }
510
- );
511
- }
512
- field.children.forEach(function (child, childIndex) {
513
- validateFieldDefinition(child, fieldPath + '.children[' + childIndex + ']', Object.assign({}, options, {
514
- disallowPresentation: true,
515
- }));
516
- });
472
+ function validateSingleFieldDefinition(field, rootPath) {
473
+ const diagnostics = collectSingleFieldValidationDiagnostics(field, rootPath);
474
+ throwFieldValidationDiagnostics(field, diagnostics, rootPath);
517
475
  }
518
476
 
519
- function validateFormFieldDefinitions(fields, rootPath) {
520
- if (!Array.isArray(fields)) {
521
- throwInvalidFieldDefinition(
522
- '字段定义必须是数组',
523
- 'CREATE_FORM_INVALID_FIELD_DEFINITION',
524
- null,
525
- rootPath || 'fields'
526
- );
477
+ function validateChangeFieldDefinitions(changes) {
478
+ if (!Array.isArray(changes)) {
479
+ return;
527
480
  }
528
- fields.forEach(function (field, index) {
529
- validateFieldDefinition(field, (rootPath || 'fields') + '[' + index + ']');
481
+ changes.forEach(function (change, changeIndex) {
482
+ if (change && change.action === 'add') {
483
+ validateSingleFieldDefinition(change.field, 'changes[' + changeIndex + '].field');
484
+ }
530
485
  });
531
486
  }
532
487
 
@@ -5095,6 +5050,24 @@ async function saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, ver
5095
5050
 
5096
5051
  // ── create 模式主流程 ─────────────────────────────────
5097
5052
 
5053
+ async function mainValidateFields(parsedArgs) {
5054
+ const { fieldsJsonOrFile } = parsedArgs;
5055
+ assertNoEmojiInDefinitionFileName(fieldsJsonOrFile);
5056
+ const { fields } = readFieldsDefinition(fieldsJsonOrFile);
5057
+ const diagnostics = collectFormFieldValidationDiagnostics(fields, { rootPath: 'fields' });
5058
+ const output = {
5059
+ success: diagnostics.length === 0,
5060
+ valid: diagnostics.length === 0,
5061
+ fieldCount: Array.isArray(fields) ? fields.length : 0,
5062
+ diagnostics,
5063
+ };
5064
+ console.log(JSON.stringify(output, null, parsedArgs.json ? 2 : 0));
5065
+ if (diagnostics.length > 0) {
5066
+ process.exitCode = 1;
5067
+ }
5068
+ return output;
5069
+ }
5070
+
5098
5071
  async function mainCreate(parsedArgs, authRef) {
5099
5072
  const { appType, formTitle, fieldsJsonOrFile, layout, theme, labelAlign } = parsedArgs;
5100
5073
 
@@ -6030,7 +6003,21 @@ async function mainUpdate(parsedArgs, authRef) {
6030
6003
  label('Form UUID:', formUuid);
6031
6004
  label('Changes:', changesJsonOrFile);
6032
6005
 
6033
- step(2, t('create_form.step_get_schema', 2));
6006
+ step(2, t('create_form.step_read_changes', 2));
6007
+ const changes = readChangesDefinition(changesJsonOrFile);
6008
+ validateChangeFieldDefinitions(changes);
6009
+ success(t('create_form.changes_loaded', changes.length));
6010
+ changes.forEach(function (change, changeIndex) {
6011
+ if (change.action === 'add') {
6012
+ listItem((changeIndex + 1) + '. [' + t('create_form.action_add') + '] ' + change.field.type + ': ' + change.field.label);
6013
+ } else if (change.action === 'delete') {
6014
+ listItem((changeIndex + 1) + '. [' + t('create_form.action_delete') + '] ' + change.label);
6015
+ } else if (change.action === 'update') {
6016
+ listItem((changeIndex + 1) + '. [' + t('create_form.action_update') + '] ' + change.label + ' → ' + Object.keys(change.changes || {}).join(', '));
6017
+ }
6018
+ });
6019
+
6020
+ step(3, t('create_form.step_get_schema', 3));
6034
6021
  info(t('create_form.sending_get_schema'));
6035
6022
  const schemaResult = await requestWithAutoLogin(function (auth) {
6036
6023
  return sendGetRequest(
@@ -6082,7 +6069,7 @@ async function mainUpdate(parsedArgs, authRef) {
6082
6069
  warn(t('create_form.schema_got_empty'));
6083
6070
  }
6084
6071
 
6085
- step(3, t('create_form.step_check_data', 3));
6072
+ step(4, t('create_form.step_check_data', 4));
6086
6073
  const dataCheckResult = await requestWithAutoLogin(function (auth) {
6087
6074
  return sendGetRequest(
6088
6075
  auth.baseUrl,
@@ -6122,19 +6109,6 @@ async function mainUpdate(parsedArgs, authRef) {
6122
6109
  success(t('create_form.data_check_empty'));
6123
6110
  }
6124
6111
 
6125
- step(4, t('create_form.step_read_changes', 4));
6126
- const changes = readChangesDefinition(changesJsonOrFile);
6127
- success(t('create_form.changes_loaded', changes.length));
6128
- changes.forEach(function (change, changeIndex) {
6129
- if (change.action === 'add') {
6130
- listItem((changeIndex + 1) + '. [' + t('create_form.action_add') + '] ' + change.field.type + ': ' + change.field.label);
6131
- } else if (change.action === 'delete') {
6132
- listItem((changeIndex + 1) + '. [' + t('create_form.action_delete') + '] ' + change.label);
6133
- } else if (change.action === 'update') {
6134
- listItem((changeIndex + 1) + '. [' + t('create_form.action_update') + '] ' + change.label + ' → ' + Object.keys(change.changes || {}).join(', '));
6135
- }
6136
- });
6137
-
6138
6112
  step(5, t('create_form.step_apply_changes', 5));
6139
6113
  const appliedChanges = applyChangesToSchema(schema, changes);
6140
6114
  const fieldDiagnostics = appliedChanges.diagnostics || [];
@@ -6215,6 +6189,9 @@ async function run(args) {
6215
6189
  if (parsedArgs.help) {
6216
6190
  return parsedArgs;
6217
6191
  }
6192
+ if (parsedArgs.mode === 'validate-fields') {
6193
+ return mainValidateFields(parsedArgs);
6194
+ }
6218
6195
 
6219
6196
  step(1, t('common.step_login', 1));
6220
6197
  const authRef = createAuthRef();
@@ -6232,6 +6209,7 @@ async function run(args) {
6232
6209
  validation: mainValidation,
6233
6210
  bindDataSource: mainBindDataSource,
6234
6211
  addOption: mainAddOption,
6212
+ validateFields: mainValidateFields,
6235
6213
  }
6236
6214
  );
6237
6215
  }
@@ -6254,6 +6232,7 @@ module.exports = {
6254
6232
  collectComponentNames,
6255
6233
  normalizeFormDefinitionType,
6256
6234
  validateFormFieldDefinitions,
6235
+ collectFormFieldValidationDiagnostics,
6257
6236
  ensureDividerThemeAction,
6258
6237
  applyChangesToSchema,
6259
6238
  },
@@ -0,0 +1,560 @@
1
+ 'use strict';
2
+
3
+ const FIELD_TYPE_ALIAS = Object.freeze({
4
+ TextAreaField: 'TextareaField',
5
+ Textareafield: 'TextareaField',
6
+ textareaField: 'TextareaField',
7
+ textAreaField: 'TextareaField',
8
+ Textfield: 'TextField',
9
+ textfield: 'TextField',
10
+ Numberfield: 'NumberField',
11
+ numberfield: 'NumberField',
12
+ Selectfield: 'SelectField',
13
+ selectfield: 'SelectField',
14
+ Radiofield: 'RadioField',
15
+ radiofield: 'RadioField',
16
+ Checkboxfield: 'CheckboxField',
17
+ checkboxfield: 'CheckboxField',
18
+ Datefield: 'DateField',
19
+ datefield: 'DateField',
20
+ Tablefield: 'TableField',
21
+ tablefield: 'TableField',
22
+ Ratefield: 'RateField',
23
+ ratefield: 'RateField',
24
+ Imagefield: 'ImageField',
25
+ imagefield: 'ImageField',
26
+ Attachmentfield: 'AttachmentField',
27
+ attachmentfield: 'AttachmentField',
28
+ Employeefield: 'EmployeeField',
29
+ employeefield: 'EmployeeField',
30
+ MultiSelectfield: 'MultiSelectField',
31
+ Multiselectfield: 'MultiSelectField',
32
+ multiselectfield: 'MultiSelectField',
33
+ SerialNumberfield: 'SerialNumberField',
34
+ Serialnumberfield: 'SerialNumberField',
35
+ serialnumberfield: 'SerialNumberField',
36
+ });
37
+
38
+ const PRESENTATION_TYPE_ALIAS = Object.freeze({
39
+ Divider: 'Divider',
40
+ divider: 'Divider',
41
+ ColumnsLayout: 'ColumnContainer',
42
+ columnsLayout: 'ColumnContainer',
43
+ ColumnContainer: 'ColumnContainer',
44
+ columnContainer: 'ColumnContainer',
45
+ Column: 'Column',
46
+ column: 'Column',
47
+ GroupContainer: 'GroupContainer',
48
+ groupContainer: 'GroupContainer',
49
+ PageSection: 'PageSection',
50
+ pageSection: 'PageSection',
51
+ });
52
+
53
+ const BUSINESS_FIELD_TYPES = Object.freeze([
54
+ 'TextField',
55
+ 'TextareaField',
56
+ 'RadioField',
57
+ 'SelectField',
58
+ 'CheckboxField',
59
+ 'MultiSelectField',
60
+ 'NumberField',
61
+ 'RateField',
62
+ 'DateField',
63
+ 'CascadeDateField',
64
+ 'EmployeeField',
65
+ 'DepartmentSelectField',
66
+ 'CountrySelectField',
67
+ 'AddressField',
68
+ 'AttachmentField',
69
+ 'ImageField',
70
+ 'TableField',
71
+ 'AssociationFormField',
72
+ 'SerialNumberField',
73
+ ]);
74
+
75
+ const PRESENTATION_FIELD_TYPES = Object.freeze([
76
+ 'Divider',
77
+ 'ColumnContainer',
78
+ 'Column',
79
+ 'GroupContainer',
80
+ 'PageSection',
81
+ ]);
82
+
83
+ const OPTION_FIELD_TYPES = Object.freeze([
84
+ 'SelectField',
85
+ 'RadioField',
86
+ 'CheckboxField',
87
+ 'MultiSelectField',
88
+ ]);
89
+
90
+ const READABLE_I18N_LABEL_KEYS = Object.freeze([
91
+ 'zh_CN',
92
+ 'en_US',
93
+ 'ja_JP',
94
+ 'zh_TW',
95
+ 'zh_HK',
96
+ 'pureEn_US',
97
+ 'name',
98
+ ]);
99
+
100
+ function isPlainObject(value) {
101
+ return !!value && typeof value === 'object' && !Array.isArray(value);
102
+ }
103
+
104
+ function describeActual(value) {
105
+ if (Array.isArray(value)) {
106
+ return 'array';
107
+ }
108
+ if (value === null) {
109
+ return 'null';
110
+ }
111
+ return typeof value;
112
+ }
113
+
114
+ function readDefinitionType(field) {
115
+ if (!isPlainObject(field)) {
116
+ return '';
117
+ }
118
+ if (field.type !== undefined && field.type !== null && field.type !== '') {
119
+ return String(field.type).trim();
120
+ }
121
+ if (field.componentName !== undefined && field.componentName !== null && field.componentName !== '') {
122
+ return String(field.componentName).trim();
123
+ }
124
+ if (field.componentType !== undefined && field.componentType !== null && field.componentType !== '') {
125
+ return String(field.componentType).trim();
126
+ }
127
+ return '';
128
+ }
129
+
130
+ function normalizeDefinitionType(field) {
131
+ const rawType = readDefinitionType(field);
132
+ if (!rawType) {
133
+ return '';
134
+ }
135
+ return PRESENTATION_TYPE_ALIAS[rawType] || FIELD_TYPE_ALIAS[rawType] || rawType;
136
+ }
137
+
138
+ function isBusinessFieldType(type) {
139
+ return BUSINESS_FIELD_TYPES.indexOf(type) !== -1;
140
+ }
141
+
142
+ function isPresentationFieldType(type) {
143
+ return PRESENTATION_FIELD_TYPES.indexOf(type) !== -1;
144
+ }
145
+
146
+ function hasNonEmptyText(value) {
147
+ return typeof value === 'string' && value.trim().length > 0;
148
+ }
149
+
150
+ function hasNonEmptyI18nObject(value) {
151
+ if (!isPlainObject(value)) {
152
+ return false;
153
+ }
154
+ return READABLE_I18N_LABEL_KEYS.some(function (key) {
155
+ return hasNonEmptyText(value[key]);
156
+ });
157
+ }
158
+
159
+ function hasFieldLabel(value) {
160
+ return hasNonEmptyText(value) || hasNonEmptyI18nObject(value);
161
+ }
162
+
163
+ function hasFixedOptionSource(field) {
164
+ return Array.isArray(field.dataSource) && field.dataSource.length > 0;
165
+ }
166
+
167
+ function hasLegacyOptionSource(field) {
168
+ return Array.isArray(field.options) && field.options.length > 0;
169
+ }
170
+
171
+ function hasRemoteOptionSource(field) {
172
+ return !!(
173
+ field.remoteDataSource ||
174
+ field.searchDataSource ||
175
+ field.dataSourceConfig ||
176
+ field.dataSourceUrl ||
177
+ field.searchConfig
178
+ );
179
+ }
180
+
181
+ function hasAssociationForm(field) {
182
+ return isPlainObject(field.associationForm) && hasNonEmptyText(field.associationForm.formUuid);
183
+ }
184
+
185
+ function createDiagnostic(code, path, expected, actual, message, suggestion) {
186
+ return {
187
+ code,
188
+ path,
189
+ expected,
190
+ actual,
191
+ message,
192
+ suggestion,
193
+ };
194
+ }
195
+
196
+ function pushDiagnostic(diagnostics, code, path, expected, actual, message, suggestion) {
197
+ diagnostics.push(createDiagnostic(code, path, expected, actual, message, suggestion));
198
+ }
199
+
200
+ function validateChildrenPropertyShape(field, path, diagnostics, expectedDescription) {
201
+ if (field.children === undefined || field.children === null) {
202
+ return true;
203
+ }
204
+ if (!Array.isArray(field.children)) {
205
+ pushDiagnostic(
206
+ diagnostics,
207
+ 'INVALID_CHILDREN_SHAPE',
208
+ path + '.children',
209
+ expectedDescription,
210
+ describeActual(field.children),
211
+ 'children must be an array with the expected depth for this component.',
212
+ 'Rewrite children to match the component-specific shape before creating or updating the form.'
213
+ );
214
+ return false;
215
+ }
216
+ return true;
217
+ }
218
+
219
+ function validateDivider(field, path, diagnostics) {
220
+ if (
221
+ field.title !== undefined &&
222
+ typeof field.title !== 'string' &&
223
+ !isPlainObject(field.title)
224
+ ) {
225
+ pushDiagnostic(
226
+ diagnostics,
227
+ 'INVALID_DIVIDER_TITLE',
228
+ path + '.title',
229
+ 'string or i18n object',
230
+ describeActual(field.title),
231
+ 'Divider.title must be a string or an i18n object when provided.',
232
+ 'Use a plain string title or omit title.'
233
+ );
234
+ }
235
+ if (field.dividerType !== undefined && typeof field.dividerType !== 'string') {
236
+ pushDiagnostic(
237
+ diagnostics,
238
+ 'INVALID_DIVIDER_TYPE',
239
+ path + '.dividerType',
240
+ 'string',
241
+ describeActual(field.dividerType),
242
+ 'Divider.dividerType must be a string when provided.',
243
+ 'Use a supported divider style string or omit dividerType.'
244
+ );
245
+ }
246
+ }
247
+
248
+ function validateFieldDefinition(field, path, diagnostics, options) {
249
+ const validationOptions = options || {};
250
+
251
+ if (!isPlainObject(field)) {
252
+ pushDiagnostic(
253
+ diagnostics,
254
+ 'INVALID_FIELD_DEFINITION',
255
+ path,
256
+ 'object',
257
+ describeActual(field),
258
+ 'Field definition must be an object.',
259
+ 'Replace this item with an object such as {"type":"TextField","label":"Name"}.'
260
+ );
261
+ return;
262
+ }
263
+
264
+ const type = normalizeDefinitionType(field);
265
+ if (!type) {
266
+ pushDiagnostic(
267
+ diagnostics,
268
+ 'FIELD_TYPE_MISSING',
269
+ path + '.type',
270
+ 'non-empty supported field type',
271
+ 'missing',
272
+ 'Field definition must include type, componentName, or componentType.',
273
+ 'Add a supported type such as TextField, SelectField, TableField, or ColumnContainer.'
274
+ );
275
+ return;
276
+ }
277
+
278
+ if (!isBusinessFieldType(type) && !isPresentationFieldType(type)) {
279
+ pushDiagnostic(
280
+ diagnostics,
281
+ 'UNSUPPORTED_FIELD_TYPE',
282
+ path + '.type',
283
+ BUSINESS_FIELD_TYPES.concat(PRESENTATION_FIELD_TYPES).join(', '),
284
+ type,
285
+ 'Field type is not supported by openyida create-form.',
286
+ 'Use one of the supported field types, or add CLI support before using this type.'
287
+ );
288
+ return;
289
+ }
290
+
291
+ if (validationOptions.insideTable && type === 'TableField') {
292
+ pushDiagnostic(
293
+ diagnostics,
294
+ 'NESTED_TABLE_FIELD_UNSUPPORTED',
295
+ path,
296
+ 'non-TableField child',
297
+ 'TableField',
298
+ 'TableField.children cannot contain another TableField.',
299
+ 'Move the nested table to the top level or flatten the subtable fields.'
300
+ );
301
+ return;
302
+ }
303
+
304
+ if (validationOptions.insideTable && isPresentationFieldType(type)) {
305
+ pushDiagnostic(
306
+ diagnostics,
307
+ 'TABLE_CHILD_PRESENTATION_UNSUPPORTED',
308
+ path,
309
+ 'business field object',
310
+ type,
311
+ 'TableField.children cannot contain presentation components.',
312
+ 'Use business fields only inside TableField.children.'
313
+ );
314
+ return;
315
+ }
316
+
317
+ if (type === 'Column') {
318
+ if (!validationOptions.allowColumn) {
319
+ pushDiagnostic(
320
+ diagnostics,
321
+ 'COLUMN_OUTSIDE_COLUMN_CONTAINER',
322
+ path,
323
+ 'Column object inside ColumnContainer.children',
324
+ 'Column',
325
+ 'Column can only be used as a ColumnContainer.children Column object.',
326
+ 'Move this Column object under ColumnContainer.children, or replace it with a normal field definition.'
327
+ );
328
+ return;
329
+ }
330
+ validateColumnObject(field, path, diagnostics);
331
+ return;
332
+ }
333
+
334
+ if (isBusinessFieldType(type) && !hasFieldLabel(field.label)) {
335
+ pushDiagnostic(
336
+ diagnostics,
337
+ 'BUSINESS_FIELD_LABEL_MISSING',
338
+ path + '.label',
339
+ 'non-empty string or i18n object',
340
+ field.label === undefined ? 'missing' : describeActual(field.label),
341
+ 'Business field definitions must include a non-empty label.',
342
+ 'Add a label that users will see on the form, such as "Name" or {"zh_CN":"姓名","en_US":"Name"}.'
343
+ );
344
+ }
345
+
346
+ if (type === 'Divider') {
347
+ validateDivider(field, path, diagnostics);
348
+ return;
349
+ }
350
+
351
+ if (type === 'ColumnContainer') {
352
+ validateColumnContainer(field, path, diagnostics);
353
+ return;
354
+ }
355
+
356
+ if (type === 'GroupContainer' || type === 'PageSection') {
357
+ validateOneDimensionalChildren(field, path, diagnostics, type);
358
+ return;
359
+ }
360
+
361
+ if (type === 'TableField') {
362
+ validateTableField(field, path, diagnostics);
363
+ }
364
+
365
+ if (
366
+ OPTION_FIELD_TYPES.indexOf(type) !== -1 &&
367
+ !hasFixedOptionSource(field) &&
368
+ !hasLegacyOptionSource(field) &&
369
+ !hasRemoteOptionSource(field)
370
+ ) {
371
+ pushDiagnostic(
372
+ diagnostics,
373
+ 'OPTION_FIELD_DATASOURCE_MISSING',
374
+ path + '.dataSource',
375
+ 'non-empty dataSource/options array or remote data source config',
376
+ 'missing',
377
+ type + ' must define fixed options or a remote data source.',
378
+ 'Provide dataSource (preferred), options, remoteDataSource, dataSourceConfig, dataSourceUrl, or searchConfig.'
379
+ );
380
+ }
381
+
382
+ if (type === 'AssociationFormField' && !hasAssociationForm(field)) {
383
+ pushDiagnostic(
384
+ diagnostics,
385
+ 'ASSOCIATION_FORM_MISSING',
386
+ path + '.associationForm',
387
+ 'object with non-empty formUuid',
388
+ field.associationForm === undefined ? 'missing' : describeActual(field.associationForm),
389
+ 'AssociationFormField must include associationForm.formUuid.',
390
+ 'Set associationForm to the target form metadata before creating or updating the form.'
391
+ );
392
+ }
393
+
394
+ if (
395
+ (type === 'EmployeeField' || type === 'DepartmentSelectField') &&
396
+ field.multiple !== undefined &&
397
+ typeof field.multiple !== 'boolean'
398
+ ) {
399
+ pushDiagnostic(
400
+ diagnostics,
401
+ 'INVALID_MULTIPLE_TYPE',
402
+ path + '.multiple',
403
+ 'boolean',
404
+ describeActual(field.multiple),
405
+ type + '.multiple must be a boolean when provided.',
406
+ 'Use true or false instead of strings or numbers.'
407
+ );
408
+ }
409
+ }
410
+
411
+ function validateColumnObject(field, path, diagnostics) {
412
+ if (!validateChildrenPropertyShape(field, path, diagnostics, 'FieldDefinition[]')) {
413
+ return;
414
+ }
415
+ if (field.children === undefined || field.children === null) {
416
+ return;
417
+ }
418
+ field.children.forEach(function (child, childIndex) {
419
+ const childPath = path + '.children[' + childIndex + ']';
420
+ if (Array.isArray(child)) {
421
+ pushDiagnostic(
422
+ diagnostics,
423
+ 'INVALID_COLUMN_CONTAINER_CHILDREN_DEPTH',
424
+ childPath,
425
+ 'FieldDefinition object',
426
+ 'array',
427
+ 'Column.children must be a one-dimensional field array.',
428
+ 'Remove the extra array nesting so Column.children is FieldDefinition[].'
429
+ );
430
+ return;
431
+ }
432
+ validateFieldDefinition(child, childPath, diagnostics);
433
+ });
434
+ }
435
+
436
+ function validateColumnContainer(field, path, diagnostics) {
437
+ if (!validateChildrenPropertyShape(field, path, diagnostics, 'FieldDefinition[][] or Column[]')) {
438
+ return;
439
+ }
440
+ if (field.children === undefined || field.children === null) {
441
+ return;
442
+ }
443
+ field.children.forEach(function (columnChildren, columnIndex) {
444
+ const columnPath = path + '.children[' + columnIndex + ']';
445
+ if (isPlainObject(columnChildren) && normalizeDefinitionType(columnChildren) === 'Column') {
446
+ validateFieldDefinition(columnChildren, columnPath, diagnostics, { allowColumn: true });
447
+ return;
448
+ }
449
+ if (!Array.isArray(columnChildren)) {
450
+ pushDiagnostic(
451
+ diagnostics,
452
+ 'INVALID_COLUMN_CONTAINER_CHILDREN_DEPTH',
453
+ columnPath,
454
+ 'array of FieldDefinition objects or Column object',
455
+ describeActual(columnChildren),
456
+ 'ColumnContainer.children must contain two-dimensional field arrays or Column objects.',
457
+ 'Use children: [[fieldA], [fieldB]] or children: [{ type: "Column", children: [fieldA] }].'
458
+ );
459
+ return;
460
+ }
461
+ columnChildren.forEach(function (child, childIndex) {
462
+ const childPath = columnPath + '[' + childIndex + ']';
463
+ if (Array.isArray(child)) {
464
+ pushDiagnostic(
465
+ diagnostics,
466
+ 'INVALID_COLUMN_CONTAINER_CHILDREN_DEPTH',
467
+ childPath,
468
+ 'FieldDefinition object',
469
+ 'array',
470
+ 'ColumnContainer.children must be exactly two levels deep.',
471
+ 'Remove the extra array nesting so children is FieldDefinition[][].'
472
+ );
473
+ return;
474
+ }
475
+ validateFieldDefinition(child, childPath, diagnostics);
476
+ });
477
+ });
478
+ }
479
+
480
+ function validateOneDimensionalChildren(field, path, diagnostics, type) {
481
+ if (!validateChildrenPropertyShape(field, path, diagnostics, 'FieldDefinition[]')) {
482
+ return;
483
+ }
484
+ if (field.children === undefined || field.children === null) {
485
+ return;
486
+ }
487
+ field.children.forEach(function (child, childIndex) {
488
+ const childPath = path + '.children[' + childIndex + ']';
489
+ if (Array.isArray(child)) {
490
+ pushDiagnostic(
491
+ diagnostics,
492
+ 'INVALID_' + type.toUpperCase() + '_CHILDREN_DEPTH',
493
+ childPath,
494
+ 'FieldDefinition object',
495
+ 'array',
496
+ type + '.children must be a one-dimensional field array.',
497
+ 'Remove extra array nesting from ' + type + '.children.'
498
+ );
499
+ return;
500
+ }
501
+ validateFieldDefinition(child, childPath, diagnostics);
502
+ });
503
+ }
504
+
505
+ function validateTableField(field, path, diagnostics) {
506
+ if (!validateChildrenPropertyShape(field, path, diagnostics, 'FieldDefinition[]')) {
507
+ return;
508
+ }
509
+ if (field.children === undefined || field.children === null) {
510
+ return;
511
+ }
512
+ field.children.forEach(function (child, childIndex) {
513
+ const childPath = path + '.children[' + childIndex + ']';
514
+ if (Array.isArray(child)) {
515
+ pushDiagnostic(
516
+ diagnostics,
517
+ 'INVALID_TABLE_FIELD_CHILDREN_DEPTH',
518
+ childPath,
519
+ 'FieldDefinition object',
520
+ 'array',
521
+ 'TableField.children must be a one-dimensional field array.',
522
+ 'Remove extra array nesting from TableField.children.'
523
+ );
524
+ return;
525
+ }
526
+ validateFieldDefinition(child, childPath, diagnostics, { insideTable: true });
527
+ });
528
+ }
529
+
530
+ function validateFormFieldDefinitions(fields, options) {
531
+ const validationOptions = options || {};
532
+ const rootPath = validationOptions.rootPath || 'fields';
533
+ const diagnostics = [];
534
+
535
+ if (!Array.isArray(fields)) {
536
+ pushDiagnostic(
537
+ diagnostics,
538
+ 'INVALID_FIELDS_ROOT',
539
+ rootPath,
540
+ 'FieldDefinition[]',
541
+ describeActual(fields),
542
+ 'Field definitions root must be an array.',
543
+ 'Use an array of field definitions or an object with a fields array.'
544
+ );
545
+ return diagnostics;
546
+ }
547
+
548
+ fields.forEach(function (field, index) {
549
+ validateFieldDefinition(field, rootPath + '[' + index + ']', diagnostics);
550
+ });
551
+
552
+ return diagnostics;
553
+ }
554
+
555
+ module.exports = {
556
+ validateFormFieldDefinitions,
557
+ normalizeDefinitionType,
558
+ BUSINESS_FIELD_TYPES,
559
+ PRESENTATION_FIELD_TYPES,
560
+ };
@@ -358,6 +358,7 @@ const COMMAND_SIDE_EFFECTS = new Map([
358
358
  'agent-capabilities',
359
359
  'check-page',
360
360
  'commands',
361
+ 'create-form.validate-fields',
361
362
  'dingtalk-link',
362
363
  'formula.evaluate',
363
364
  'integration.diagnose',
@@ -616,6 +617,7 @@ const COMMAND_PERMISSIONS = new Map([
616
617
  'connector.list',
617
618
  'connector.list-actions',
618
619
  'connector.list-connections',
620
+ 'create-form.validate-fields',
619
621
  'dingtalk-link',
620
622
  'dws.contact-user-search',
621
623
  'formula.evaluate',
@@ -1025,6 +1027,9 @@ const COMMAND_GROUPS = [
1025
1027
  titleKey: 'help.group_form',
1026
1028
  commands: [
1027
1029
  command('create-form.create', ['create-form', 'create'], 'create-form create <appType> ... [--locale zh_CN|en_US|ja_JP] [--open|--no-open]', 'help.cmd_create_form'),
1030
+ command('create-form.validate-fields', ['create-form', 'validate-fields'], 'create-form validate-fields <fieldsJsonOrFile> [--json]', 'help.cmd_validate_form', {
1031
+ requiresLogin: false,
1032
+ }),
1028
1033
  command('create-form.update', ['create-form', 'update'], 'create-form update <appType> ... [--locale zh_CN|en_US|ja_JP] [--open|--no-open]', 'help.cmd_update_form'),
1029
1034
  command('create-form.patch', ['create-form', 'patch'], 'create-form patch <appType> <formUuid> <patchJsonOrFile> [--open|--no-open]', 'help.cmd_update_form'),
1030
1035
  command('create-form.rule', ['create-form', 'rule'], 'create-form rule <appType> <formUuid> <rulesJsonOrFile> [--open|--no-open]', 'help.cmd_update_form'),
@@ -30,6 +30,7 @@ module.exports = {
30
30
  cmd_import: 'Import migration package, rebuild app',
31
31
  group_form: 'Forms & Pages',
32
32
  cmd_create_form: 'Create a form page',
33
+ cmd_validate_form: 'Validate form field JSON locally',
33
34
  cmd_update_form: 'Update a form page',
34
35
  cmd_list_forms: 'List forms/pages in an app',
35
36
  cmd_aggregate_table: 'Manage aggregate tables (virtualView)',
@@ -30,6 +30,7 @@ module.exports = {
30
30
  cmd_import: '导入迁移包,重建应用',
31
31
  group_form: '表单 & 页面',
32
32
  cmd_create_form: '创建表单页面',
33
+ cmd_validate_form: '本地校验表单字段 JSON',
33
34
  cmd_update_form: '更新表单页面',
34
35
  cmd_list_forms: '列出应用下的表单/页面',
35
36
  cmd_aggregate_table: '管理聚合表(virtualView)',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.7.29-beta.2",
3
+ "version": "2026.7.30",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -219,8 +219,8 @@ openyida create-form rule <appType> <formUuid> <rulesJsonOrFile>
219
219
  | `TextField` / `TextareaField` | 单行 / 多行文本 | 最常用文本字段 |
220
220
  | `NumberField` | 数字 | 金额、数量、分值 |
221
221
  | `DateField` / `CascadeDateField` | 日期 / 日期区间 | 流程表单常用 |
222
- | `SelectField` / `RadioField` | 单选 | 固定选项用 `dataSource` |
223
- | `CheckboxField` / `MultiSelectField` | 多选 | 固定选项用 `dataSource` |
222
+ | `SelectField` / `RadioField` | 单选 | 创建字段 JSON 时必须提供 `dataSource`,不要省略或只写旧式 `options` |
223
+ | `CheckboxField` / `MultiSelectField` | 多选 | 创建字段 JSON 时必须提供 `dataSource`,不要省略或只写旧式 `options` |
224
224
  | `EmployeeField` | 成员 | 细节见 [employee-field.md](references/employee-field.md) |
225
225
  | `DepartmentSelectField` | 部门 | 支持 `multiple` |
226
226
  | `AttachmentField` / `ImageField` | 附件 / 图片 | 表单内上传能力 |
@@ -244,7 +244,7 @@ openyida create-form rule <appType> <formUuid> <rulesJsonOrFile>
244
244
 
245
245
  - `appType` 必须来自已创建应用或用户提供
246
246
  - 字段类型必须使用标准组件名,如 `TextField`、`SelectField`
247
- - `SelectField`、`RadioField`、`CheckboxField`、`MultiSelectField` 固定选项必须提供 `dataSource`
247
+ - `SelectField`、`MultiSelectField`、`RadioField`、`CheckboxField` 固定选项必须提供 `dataSource`;远程选项字段必须提供 `remoteDataSource` 或通过 `bind-datasource` 配置,不要生成无选项源的字段 JSON。
248
248
  - `TableField` 必须提供 `children`,且子表不能嵌套子表
249
249
  - `AssociationFormField` 必须提供 `associationForm`
250
250
  - update / add-option / bind-datasource / validation / rule 按字段 `label`、`fieldId` 或 `tableLabel + label` 解析并要求唯一命中;如果有重名字段,先看命令返回的 `diagnostics[].candidates`,可用 `tableLabel` 或 `fieldId` 缩小范围,仍不明确时再用 `get-schema --compact --resolve-fields`。
@@ -42,6 +42,8 @@
42
42
  | `children` | Object[] | 条件必填 | `TableField` / 展示布局组件必填 |
43
43
  | `associationForm` | Object | 条件必填 | `AssociationFormField` 必填 |
44
44
 
45
+ 选项类字段包括 `SelectField`、`MultiSelectField`、`RadioField`、`CheckboxField`。固定选项必须在字段 JSON 中提供非空 `dataSource`;不要省略选项源,也不要只写旧式 `options`。
46
+
45
47
  ## 展示/布局组件
46
48
 
47
49
  ### Divider