openyida 2026.8.31 → 2026.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -269,6 +269,7 @@ function buildCreateAppPayload(params, auth, contentLocale, openExclusive, openP
269
269
  openPhysicColumn,
270
270
  openIsolationDatabase: 'n',
271
271
  openExclusiveUnit: 'n',
272
+ createWithModernTheme: 'y',
272
273
  group: 'ALL',
273
274
  fromBuilderAi: 'y',
274
275
  builderAiSource: resolveBuilderAiSource(),
@@ -84,6 +84,13 @@ const { dispatchCreateFormCommand } = require('./create-form/commands');
84
84
  const { buildApiPath } = require('./create-form/api-path');
85
85
  const { deepMerge, splitJsonPointer } = require('./create-form/schema-patch');
86
86
  const { ensureYidaGlobalThemeAction } = require('./form-theme-action');
87
+ const {
88
+ actionRefName,
89
+ buildDesignerEventBinding,
90
+ extractExportedActionFunctionNames,
91
+ inspectActionBindings,
92
+ syncDesignerActionCatalog,
93
+ } = require('./form-action-binding');
87
94
  const { FORM_RULES_BLOCK_END, FORM_RULES_BLOCK_START } = require('./create-form/rule-builder');
88
95
  const { SMART_VALIDATION_BLOCK_END, SMART_VALIDATION_BLOCK_START } = require('./create-form/validation-builder');
89
96
  const { createFieldNormalizers } = require('./create-form/field-normalizers');
@@ -2678,6 +2685,116 @@ function compileActionSource(source) {
2678
2685
  return compiledResult.compiled;
2679
2686
  }
2680
2687
 
2688
+ function bindFieldAction(schema, formContainer, operation) {
2689
+ if (!formContainer || !formContainer.children) {
2690
+ throw new Error('未找到 FormContainer');
2691
+ }
2692
+ const fieldKey = operation.fieldId || operation.field || operation.label;
2693
+ const eventName = operation.event || 'onChange';
2694
+ const actionName = operation.name || operation.actionName;
2695
+ if (!actionName) {
2696
+ throw new Error('bind-field-action 必须提供 name/actionName');
2697
+ }
2698
+ if (!isValidActionIdentifier(actionName)) {
2699
+ throw new Error('动作名称不是合法的 JavaScript 标识符: ' + actionName);
2700
+ }
2701
+ const found = findFieldByIdOrLabelDeep(formContainer.children, fieldKey);
2702
+ if (!found) {
2703
+ throw new Error('未找到字段: ' + fieldKey);
2704
+ }
2705
+ found.field.props = found.field.props || {};
2706
+ const existingActionName = actionRefName(found.field.props[eventName]);
2707
+ if (existingActionName && existingActionName !== actionName && operation.replaceExisting !== true) {
2708
+ throw createCreateFormError(
2709
+ t('create_form.action_event_conflict', fieldKey, eventName, existingActionName),
2710
+ 'FORM_ACTION_EVENT_CONFLICT',
2711
+ {
2712
+ status: 'SEMANTIC_FAILURE',
2713
+ retryable: false,
2714
+ sideEffectState: 'none',
2715
+ nextStep: 'review_existing_field_action',
2716
+ field: fieldKey,
2717
+ event: eventName,
2718
+ existingActionName,
2719
+ requestedActionName: actionName,
2720
+ }
2721
+ );
2722
+ }
2723
+
2724
+ found.field.props[eventName] = buildDesignerEventBinding(
2725
+ actionName,
2726
+ operation.params || {},
2727
+ found.field.props[eventName]
2728
+ );
2729
+
2730
+ const actions = ensureActionModule(schema);
2731
+ const fieldId = found.field.props.fieldId || found.field.id || String(fieldKey || '');
2732
+ const relatedEventId = operation.relatedEventId || ((found.field.id || fieldId) + ':' + eventName);
2733
+ syncDesignerActionCatalog(actions);
2734
+
2735
+ return {
2736
+ action: operation.action === 'field-action' ? 'field-action' : 'bind-field-action',
2737
+ field: fieldKey,
2738
+ fieldId,
2739
+ event: eventName,
2740
+ actionName,
2741
+ relatedEventId,
2742
+ replacedActionName: existingActionName && existingActionName !== actionName ? existingActionName : '',
2743
+ };
2744
+ }
2745
+
2746
+ function applyAtomicFieldAction(schema, formContainer, operation) {
2747
+ const actionName = operation.name || operation.actionName;
2748
+ if (!actionName) {
2749
+ throw new Error('field-action 必须提供 name/actionName');
2750
+ }
2751
+ if (!isValidActionIdentifier(actionName)) {
2752
+ throw new Error('动作名称不是合法的 JavaScript 标识符: ' + actionName);
2753
+ }
2754
+
2755
+ const actions = ensureActionModule(schema);
2756
+ const source = readOptionalSource(operation, 'source', 'sourceFile');
2757
+ if (source !== undefined) {
2758
+ if (!extractExportedActionFunctionNames(source).includes(actionName)) {
2759
+ throw createCreateFormError(
2760
+ t('create_form.action_source_export_missing', actionName),
2761
+ 'FORM_ACTION_FUNCTION_MISSING',
2762
+ {
2763
+ status: 'SEMANTIC_FAILURE',
2764
+ retryable: false,
2765
+ sideEffectState: 'none',
2766
+ nextStep: 'export_named_action_function',
2767
+ actionName,
2768
+ }
2769
+ );
2770
+ }
2771
+ const blockStart = '/* openyida:field-action:start:' + actionName + ' */';
2772
+ const blockEnd = '/* openyida:field-action:end:' + actionName + ' */';
2773
+ actions.module.source = upsertGeneratedSourceBlockWithBounds(
2774
+ actions.module.source || '',
2775
+ source,
2776
+ blockStart,
2777
+ blockEnd
2778
+ );
2779
+ actions.module.compiled = compileActionSource(actions.module.source);
2780
+ actions.type = actions.type || 'FUNCTION';
2781
+ } else if (!extractExportedActionFunctionNames(actions.module.source || '').includes(actionName)) {
2782
+ throw createCreateFormError(
2783
+ t('create_form.action_source_missing', actionName),
2784
+ 'FORM_ACTION_FUNCTION_MISSING',
2785
+ {
2786
+ status: 'SEMANTIC_FAILURE',
2787
+ retryable: false,
2788
+ sideEffectState: 'none',
2789
+ nextStep: 'provide_action_source',
2790
+ actionName,
2791
+ }
2792
+ );
2793
+ }
2794
+
2795
+ return bindFieldAction(schema, formContainer, Object.assign({}, operation, { action: 'field-action' }));
2796
+ }
2797
+
2681
2798
  function readOptionalSource(operation, key, fileKey) {
2682
2799
  if (operation[fileKey]) {
2683
2800
  const sourcePath = path.resolve(operation[fileKey]);
@@ -2773,6 +2890,11 @@ function applySchemaPatchOperations(schema, operations) {
2773
2890
  return;
2774
2891
  }
2775
2892
 
2893
+ if (action === 'field-action') {
2894
+ applied.push(applyAtomicFieldAction(schema, formContainer, operation));
2895
+ return;
2896
+ }
2897
+
2776
2898
  if (['bind-datasource', 'data-source', 'datasource', 'select-datasource'].includes(String(action).toLowerCase())) {
2777
2899
  if (!formContainer || !formContainer.children) {
2778
2900
  throw new Error('未找到 FormContainer');
@@ -2802,42 +2924,7 @@ function applySchemaPatchOperations(schema, operations) {
2802
2924
  }
2803
2925
 
2804
2926
  if (action === 'bind-field-action') {
2805
- if (!formContainer || !formContainer.children) {
2806
- throw new Error('未找到 FormContainer');
2807
- }
2808
- const fieldKey = operation.fieldId || operation.field || operation.label;
2809
- const eventName = operation.event || 'onChange';
2810
- const actionName = operation.name || operation.actionName;
2811
- if (!actionName) {
2812
- throw new Error('bind-field-action 必须提供 name/actionName');
2813
- }
2814
- const found = findFieldByIdOrLabelDeep(formContainer.children, fieldKey);
2815
- if (!found) {
2816
- throw new Error('未找到字段: ' + fieldKey);
2817
- }
2818
- found.field.props = found.field.props || {};
2819
- found.field.props[eventName] = {
2820
- name: actionName,
2821
- id: actionName,
2822
- params: operation.params || {},
2823
- type: 'actionRef',
2824
- };
2825
-
2826
- const actions = ensureActionModule(schema);
2827
- const relatedEventId = operation.relatedEventId || (found.field.id + ':' + eventName);
2828
- const existing = actions.list.find(function (item) {
2829
- return item.relatedEventId === relatedEventId && item.id === actionName;
2830
- });
2831
- if (!existing) {
2832
- actions.list.push({
2833
- relatedEventId,
2834
- name: actionName,
2835
- id: actionName,
2836
- type: 'componentEvent',
2837
- params: operation.params || {},
2838
- });
2839
- }
2840
- applied.push({ action: 'bind-field-action', field: fieldKey, event: eventName, actionName });
2927
+ applied.push(bindFieldAction(schema, formContainer, operation));
2841
2928
  return;
2842
2929
  }
2843
2930
 
@@ -3246,18 +3333,71 @@ function upsertActionListEntry(actions, entry) {
3246
3333
  actions.list.push(entry);
3247
3334
  }
3248
3335
 
3336
+ function removeObsoleteGeneratedRuleCatalogEntries(actions, activeActionNames) {
3337
+ // Only prune OpenYida-owned rule wrappers; unknown user/platform entries must survive catalog sync.
3338
+ const activeNames = new Set(activeActionNames || []);
3339
+ actions.list = (actions.list || []).filter(function (item) {
3340
+ const names = [item && item.id, item && item.name].filter(Boolean).map(String);
3341
+ const generatedNames = names.filter(function (name) {
3342
+ return name.startsWith('openyidaRuleChange_');
3343
+ });
3344
+ return generatedNames.length === 0 || generatedNames.some(function (name) {
3345
+ return activeNames.has(name);
3346
+ });
3347
+ });
3348
+ }
3349
+
3350
+ function previousRuleActionName(source, wrapperName) {
3351
+ const start = String(source || '').indexOf('export function ' + wrapperName + '(');
3352
+ if (start === -1) {return '';}
3353
+ const end = String(source || '').indexOf('return this.openyidaApplyRules', start);
3354
+ const wrapperSource = String(source || '').slice(start, end === -1 ? undefined : end);
3355
+ const match = wrapperSource.match(/typeof this\.([A-Za-z_$][A-Za-z0-9_$]*) === ["']function["']/);
3356
+ return match ? match[1] : '';
3357
+ }
3358
+
3359
+ function reconcileGeneratedRuleBindings(value, nextSourceFieldIds, actionsSource) {
3360
+ if (!value) {return;}
3361
+ if (Array.isArray(value)) {
3362
+ value.forEach(function (item) {
3363
+ reconcileGeneratedRuleBindings(item, nextSourceFieldIds, actionsSource);
3364
+ });
3365
+ return;
3366
+ }
3367
+ if (typeof value !== 'object') {return;}
3368
+
3369
+ const props = value.props || {};
3370
+ const fieldId = String(props.fieldId || '');
3371
+ const existingActionName = actionRefName(props.onChange);
3372
+ if (fieldId && existingActionName.startsWith('openyidaRuleChange_') && !nextSourceFieldIds.has(fieldId)) {
3373
+ const previousActionName = previousRuleActionName(actionsSource, existingActionName);
3374
+ if (previousActionName) {
3375
+ props.onChange = buildDesignerEventBinding(previousActionName, {}, props.onChange);
3376
+ } else {
3377
+ delete props.onChange;
3378
+ }
3379
+ }
3380
+
3381
+ Object.values(value).forEach(function (child) {
3382
+ if (child !== props) {
3383
+ reconcileGeneratedRuleBindings(child, nextSourceFieldIds, actionsSource);
3384
+ }
3385
+ });
3386
+ }
3387
+
3249
3388
  function buildFormRulesActionSource(rules, fieldMap, bindings) {
3250
3389
  const rulesJson = JSON.stringify(rules, null, 2);
3251
3390
  const fieldMapJson = JSON.stringify(fieldMap, null, 2);
3252
3391
  const wrapperSource = bindings.map(function (binding) {
3253
3392
  const previousCall = binding.previousActionName && isValidActionIdentifier(binding.previousActionName)
3254
- ? ' if (typeof ' + binding.previousActionName + ' === "function") {\n ' + binding.previousActionName + '.call(this, event);\n }\n'
3393
+ ? ' if (typeof this.' + binding.previousActionName + ' === "function") {\n this.' + binding.previousActionName + '(event);\n }\n'
3255
3394
  : '';
3256
- return 'export function ' + binding.wrapperName + '(event) {\n' +
3395
+ const title = binding.label ? '/** @title ' + binding.label + '值发生变化 */\n' : '';
3396
+ return title + 'export function ' + binding.wrapperName + '(event) {\n' +
3257
3397
  ' event = event || {};\n' +
3258
3398
  ' event.__openyidaSourceFieldId = "' + binding.fieldId + '";\n' +
3259
3399
  previousCall +
3260
- ' return openyidaApplyRules.call(this, event);\n' +
3400
+ ' return this.openyidaApplyRules(event);\n' +
3261
3401
  '}';
3262
3402
  }).join('\n\n');
3263
3403
 
@@ -3405,10 +3545,10 @@ export function openyidaApplyRules(event) {
3405
3545
  }
3406
3546
 
3407
3547
  export function openyidaRulesDidMount(event) {
3408
- if (typeof didMount === 'function') {
3409
- didMount.call(this, event);
3548
+ if (typeof this.didMount === 'function') {
3549
+ this.didMount(event);
3410
3550
  }
3411
- return openyidaApplyRules.call(this, event || {});
3551
+ return this.openyidaApplyRules(event || {});
3412
3552
  }
3413
3553
 
3414
3554
  ${wrapperSource}
@@ -3434,6 +3574,8 @@ function applyFormRules(schema, rawRules) {
3434
3574
  }
3435
3575
  });
3436
3576
 
3577
+ reconcileGeneratedRuleBindings(formContainer.children, sourceFieldIds, actions.module.source || '');
3578
+
3437
3579
  const bindings = [];
3438
3580
  sourceFieldIds.forEach(function (fieldId) {
3439
3581
  const found = findFieldByIdOrLabelDeep(formContainer.children, fieldId);
@@ -3443,32 +3585,18 @@ function applyFormRules(schema, rawRules) {
3443
3585
  found.field.props = found.field.props || {};
3444
3586
  const eventName = 'onChange';
3445
3587
  const existingAction = found.field.props[eventName];
3446
- const previousActionName = existingAction &&
3447
- existingAction.type === 'actionRef' &&
3448
- existingAction.name &&
3449
- !String(existingAction.name).startsWith('openyidaRuleChange_')
3450
- ? String(existingAction.name)
3451
- : '';
3588
+ const existingActionName = actionRefName(existingAction);
3452
3589
  const wrapperName = 'openyidaRuleChange_' + sanitizeActionName(fieldId);
3590
+ const previousActionName = existingActionName.startsWith('openyidaRuleChange_')
3591
+ ? previousRuleActionName(actions.module.source || '', existingActionName)
3592
+ : existingActionName;
3453
3593
  const relatedEventId = (found.field.id || fieldId) + ':' + eventName;
3454
3594
 
3455
- found.field.props[eventName] = {
3456
- name: wrapperName,
3457
- id: wrapperName,
3458
- params: {},
3459
- type: 'actionRef',
3460
- };
3461
-
3462
- actions.list = actions.list.filter(function (item) {
3463
- return !(item.relatedEventId === relatedEventId && String(item.id || '').startsWith('openyidaRuleChange_'));
3464
- });
3465
- upsertActionListEntry(actions, {
3466
- relatedEventId,
3467
- name: wrapperName,
3468
- id: wrapperName,
3469
- type: 'componentEvent',
3470
- params: {},
3471
- });
3595
+ found.field.props[eventName] = buildDesignerEventBinding(
3596
+ wrapperName,
3597
+ {},
3598
+ found.field.props[eventName]
3599
+ );
3472
3600
 
3473
3601
  bindings.push({
3474
3602
  fieldId,
@@ -3516,6 +3644,11 @@ function applyFormRules(schema, rawRules) {
3516
3644
  actions.module.source = upsertGeneratedSourceBlock(actions.module.source || '', generatedSource);
3517
3645
  actions.module.compiled = compileActionSource(actions.module.source);
3518
3646
  actions.type = actions.type || 'FUNCTION';
3647
+ removeObsoleteGeneratedRuleCatalogEntries(actions, bindings.map(function (binding) {
3648
+ return binding.wrapperName;
3649
+ }));
3650
+ // Merge source exports after the rich lifecycle/component entries above so their metadata is preserved.
3651
+ syncDesignerActionCatalog(actions);
3519
3652
 
3520
3653
  return {
3521
3654
  rules: normalized.rules,
@@ -5332,7 +5465,22 @@ async function mainRule(parsedArgs, authRef) {
5332
5465
  fillSerialNumberFormulas(formContainer.children, corpId, appType, formUuid);
5333
5466
  }
5334
5467
 
5468
+ const actionBindingExpectations = applied.bindings.map(function (binding) {
5469
+ return {
5470
+ fieldId: binding.fieldId,
5471
+ event: binding.eventName,
5472
+ actionName: binding.wrapperName,
5473
+ relatedEventId: binding.relatedEventId,
5474
+ };
5475
+ });
5476
+ assertFormActionBindings(schema, actionBindingExpectations);
5335
5477
  await saveFormSchema(authRef, appType, formUuid, schema, version, 4);
5478
+ const actionReadback = await readBackFormActionBindings(
5479
+ authRef,
5480
+ appType,
5481
+ formUuid,
5482
+ actionBindingExpectations
5483
+ );
5336
5484
 
5337
5485
  const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
5338
5486
  result(true, '表单联动规则保存成功', [
@@ -5347,6 +5495,7 @@ async function mainRule(parsedArgs, authRef) {
5347
5495
  formUuid,
5348
5496
  appType,
5349
5497
  rulesApplied: applied.rules.length,
5498
+ readbackVerified: actionReadback.verified,
5350
5499
  rules: applied.rules.map(function (rule) {
5351
5500
  return {
5352
5501
  type: rule.type,
@@ -5358,11 +5507,16 @@ async function mainRule(parsedArgs, authRef) {
5358
5507
  };
5359
5508
  }),
5360
5509
  eventBindings: applied.bindings.map(function (binding) {
5510
+ const readback = actionReadback.bindings.find(function (item) {
5511
+ return item.fieldId === binding.fieldId && item.event === binding.eventName;
5512
+ });
5361
5513
  return {
5362
5514
  fieldId: binding.fieldId,
5363
5515
  label: binding.label,
5364
5516
  event: binding.eventName,
5365
5517
  actionName: binding.wrapperName,
5518
+ designerBindingFound: readback && readback.designerBindingFound,
5519
+ verified: readback && readback.verified,
5366
5520
  resolved: getFieldEvidenceFromLookup(applied.fieldLookup, binding.fieldId),
5367
5521
  };
5368
5522
  }),
@@ -5376,6 +5530,97 @@ async function mainRule(parsedArgs, authRef) {
5376
5530
 
5377
5531
  // ── patch 模式主流程 ──────────────────────────────────
5378
5532
 
5533
+ function actionBindingExpectationsFromOperations(appliedOperations) {
5534
+ return (appliedOperations || []).filter(function (operation) {
5535
+ return operation.action === 'field-action' || operation.action === 'bind-field-action';
5536
+ }).map(function (operation) {
5537
+ return {
5538
+ fieldId: operation.fieldId,
5539
+ event: operation.event,
5540
+ actionName: operation.actionName,
5541
+ relatedEventId: operation.relatedEventId,
5542
+ };
5543
+ });
5544
+ }
5545
+
5546
+ function assertFormActionBindings(schema, expectations, options) {
5547
+ const inspection = inspectActionBindings(schema, expectations);
5548
+ if (inspection.verified) {
5549
+ return inspection;
5550
+ }
5551
+ const readback = options && options.readback === true;
5552
+ throw createCreateFormError(
5553
+ t(readback ? 'create_form.action_readback_mismatch' : 'create_form.action_binding_incomplete'),
5554
+ readback ? 'FORM_ACTION_BINDING_READBACK_MISMATCH' : 'FORM_ACTION_BINDING_INCOMPLETE',
5555
+ {
5556
+ status: 'SEMANTIC_FAILURE',
5557
+ retryable: false,
5558
+ retrySafe: false,
5559
+ sideEffectState: readback ? 'committed' : 'none',
5560
+ mutationAccepted: readback,
5561
+ readbackVerified: false,
5562
+ readbackAllowed: true,
5563
+ nextStep: readback ? 'inspect_existing_form_action_binding' : 'fix_action_binding_patch',
5564
+ bindings: inspection.bindings,
5565
+ }
5566
+ );
5567
+ }
5568
+
5569
+ async function readBackFormActionBindings(authRef, appType, formUuid, expectations) {
5570
+ const schemaResult = await requestWithAutoLogin(function (auth) {
5571
+ return sendGetRequest(
5572
+ auth.baseUrl,
5573
+ buildApiPath(appType, 'getFormSchema', { prefix: '_view', namespace: 'alibaba' }),
5574
+ { formUuid: formUuid, schemaVersion: 'V5' }
5575
+ );
5576
+ }, authRef);
5577
+
5578
+ if (!schemaResult || schemaResult.success === false || schemaResult.__needLogin) {
5579
+ const errorMsg = schemaResult ? schemaResult.errorMsg || t('common.unknown_error') : t('common.request_failed');
5580
+ throw createCreateFormError(
5581
+ t('create_form.action_readback_failed', errorMsg),
5582
+ 'FORM_ACTION_BINDING_READBACK_FAILED',
5583
+ {
5584
+ status: 'READBACK_UNVERIFIED',
5585
+ retryable: true,
5586
+ retrySafe: false,
5587
+ sideEffectState: 'committed',
5588
+ mutationAccepted: true,
5589
+ readbackVerified: false,
5590
+ readbackAllowed: true,
5591
+ nextStep: 'retry_readback_only',
5592
+ target: { appType, formUuid },
5593
+ result: sanitizeFailureResult(schemaResult),
5594
+ }
5595
+ );
5596
+ }
5597
+
5598
+ let schema;
5599
+ try {
5600
+ schema = schemaResult.content
5601
+ ? (typeof schemaResult.content === 'string' ? JSON.parse(schemaResult.content) : schemaResult.content)
5602
+ : schemaResult;
5603
+ } catch (parseError) {
5604
+ throw createCreateFormError(
5605
+ t('create_form.action_readback_failed', parseError.message),
5606
+ 'FORM_ACTION_BINDING_READBACK_FAILED',
5607
+ {
5608
+ status: 'READBACK_UNVERIFIED',
5609
+ retryable: true,
5610
+ retrySafe: false,
5611
+ sideEffectState: 'committed',
5612
+ mutationAccepted: true,
5613
+ readbackVerified: false,
5614
+ readbackAllowed: true,
5615
+ nextStep: 'retry_readback_only',
5616
+ target: { appType, formUuid },
5617
+ }
5618
+ );
5619
+ }
5620
+
5621
+ return assertFormActionBindings(schema, expectations, { readback: true });
5622
+ }
5623
+
5379
5624
  async function mainPatch(parsedArgs, authRef) {
5380
5625
  const { appType, formUuid, patchJsonOrFile } = parsedArgs;
5381
5626
 
@@ -5429,6 +5674,9 @@ async function mainPatch(parsedArgs, authRef) {
5429
5674
  try {
5430
5675
  appliedOperations = applySchemaPatchOperations(schema, operations);
5431
5676
  } catch (patchError) {
5677
+ if (patchError && patchError.isCliError) {
5678
+ throw patchError;
5679
+ }
5432
5680
  fail('Schema 补丁应用失败: ' + patchError.message);
5433
5681
  console.log(JSON.stringify({
5434
5682
  success: false,
@@ -5441,6 +5689,10 @@ async function mainPatch(parsedArgs, authRef) {
5441
5689
  formUuid,
5442
5690
  });
5443
5691
  }
5692
+ const actionBindingExpectations = actionBindingExpectationsFromOperations(appliedOperations);
5693
+ const localActionInspection = actionBindingExpectations.length > 0
5694
+ ? assertFormActionBindings(schema, actionBindingExpectations)
5695
+ : null;
5444
5696
  success('已应用 ' + appliedOperations.length + ' 个补丁操作');
5445
5697
  appliedOperations.forEach(function (operation, index) {
5446
5698
  listItem((index + 1) + '. ' + operation.action + (operation.field ? ' ' + operation.field : '') + (operation.path ? ' ' + operation.path : ''));
@@ -5455,6 +5707,9 @@ async function mainPatch(parsedArgs, authRef) {
5455
5707
  }
5456
5708
 
5457
5709
  await saveFormSchema(authRef, appType, formUuid, schema, version, 4);
5710
+ const actionReadback = actionBindingExpectations.length > 0
5711
+ ? await readBackFormActionBindings(authRef, appType, formUuid, actionBindingExpectations)
5712
+ : null;
5458
5713
 
5459
5714
  const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
5460
5715
  result(true, 'Schema 补丁保存成功', [
@@ -5464,7 +5719,16 @@ async function mainPatch(parsedArgs, authRef) {
5464
5719
  ]);
5465
5720
 
5466
5721
  console.log(JSON.stringify(withBrowserHandoff(
5467
- { success: true, formUuid, appType, operationsApplied: appliedOperations.length, operations: appliedOperations, url: formUrl },
5722
+ {
5723
+ success: true,
5724
+ formUuid,
5725
+ appType,
5726
+ operationsApplied: appliedOperations.length,
5727
+ operations: appliedOperations,
5728
+ readbackVerified: actionReadback ? actionReadback.verified : undefined,
5729
+ eventBindings: actionReadback ? actionReadback.bindings : (localActionInspection ? localActionInspection.bindings : undefined),
5730
+ url: formUrl,
5731
+ },
5468
5732
  formUrl,
5469
5733
  { stage: 'patch_form_success', title: formUuid },
5470
5734
  parsedArgs.browserOpenMode
@@ -5811,5 +6075,9 @@ module.exports = {
5811
6075
  ensureYidaGlobalThemeAction,
5812
6076
  applyDefaultFormDetailStyle,
5813
6077
  applyChangesToSchema,
6078
+ applyFormRules,
6079
+ applySchemaPatchOperations,
6080
+ actionBindingExpectationsFromOperations,
6081
+ assertFormActionBindings,
5814
6082
  },
5815
6083
  };
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ let designerEventCounter = 0;
4
+
5
+ function actionRefs(value) {
6
+ if (!value || typeof value !== 'object') {
7
+ return [];
8
+ }
9
+ if (value.type === 'JSExpression' && Array.isArray(value.events)) {
10
+ return value.events.filter(function (event) {
11
+ return event && event.type === 'actionRef' && (event.name || event.id);
12
+ });
13
+ }
14
+ if (value.type === 'actionRef' && (value.name || value.id)) {
15
+ return [value];
16
+ }
17
+ return [];
18
+ }
19
+
20
+ function actionRefName(value) {
21
+ const refs = actionRefs(value);
22
+ return refs.length > 0 ? String(refs[0].name || refs[0].id || '') : '';
23
+ }
24
+
25
+ function isDesignerEventBinding(value, actionName) {
26
+ if (!value || value.type !== 'JSExpression' || !Array.isArray(value.events)) {
27
+ return false;
28
+ }
29
+ const refs = actionRefs(value);
30
+ if (!refs.some(function (event) {
31
+ return String(event.name || event.id || '') === String(actionName || '');
32
+ })) {
33
+ return false;
34
+ }
35
+ const expression = String(value.value || '');
36
+ return expression.includes('legaoBuiltin.execEventFlow') &&
37
+ expression.includes('this.' + actionName);
38
+ }
39
+
40
+ function nextDesignerEventUuid() {
41
+ designerEventCounter += 1;
42
+ return String(Date.now()) + '_' + String(designerEventCounter);
43
+ }
44
+
45
+ function buildDesignerEventBinding(actionName, params, existingValue) {
46
+ const existingRef = actionRefs(existingValue).find(function (event) {
47
+ return String(event.name || event.id || '') === String(actionName || '');
48
+ });
49
+ return {
50
+ type: 'JSExpression',
51
+ value: 'this.utils.legaoBuiltin.execEventFlow.bind(this, [this.' + actionName + '])',
52
+ events: [{
53
+ name: actionName,
54
+ id: actionName,
55
+ params: params || {},
56
+ type: 'actionRef',
57
+ uuid: existingRef && existingRef.uuid || nextDesignerEventUuid(),
58
+ }],
59
+ };
60
+ }
61
+
62
+ function extractExportedActionFunctionNames(source) {
63
+ const names = [];
64
+ const pattern = /export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g;
65
+ let match;
66
+ while ((match = pattern.exec(String(source || ''))) !== null) {
67
+ if (!names.includes(match[1])) {
68
+ names.push(match[1]);
69
+ }
70
+ }
71
+ return names.sort();
72
+ }
73
+
74
+ function actionCatalogEntryNames(entry) {
75
+ return [entry && entry.id, entry && entry.name]
76
+ .filter(Boolean)
77
+ .map(String);
78
+ }
79
+
80
+ function syncDesignerActionCatalog(actions) {
81
+ const source = actions && actions.module && actions.module.source || '';
82
+ const list = Array.isArray(actions.list) ? actions.list.slice() : [];
83
+ const existingNames = new Set();
84
+ list.forEach(function (entry) {
85
+ actionCatalogEntryNames(entry).forEach(function (name) {
86
+ existingNames.add(name);
87
+ });
88
+ });
89
+ extractExportedActionFunctionNames(source).forEach(function (name) {
90
+ if (!existingNames.has(name)) {
91
+ list.push({ id: name, title: name });
92
+ existingNames.add(name);
93
+ }
94
+ });
95
+ actions.list = list;
96
+ return actions.list;
97
+ }
98
+
99
+ function findFieldById(value, fieldId) {
100
+ if (!value || !fieldId) {return null;}
101
+ if (Array.isArray(value)) {
102
+ for (const item of value) {
103
+ const found = findFieldById(item, fieldId);
104
+ if (found) {return found;}
105
+ }
106
+ return null;
107
+ }
108
+ if (typeof value !== 'object') {return null;}
109
+ if (String(value.fieldId || '') === String(fieldId) ||
110
+ value.props && String(value.props.fieldId || '') === String(fieldId)) {
111
+ return value;
112
+ }
113
+ for (const child of Object.values(value)) {
114
+ const found = findFieldById(child, fieldId);
115
+ if (found) {return found;}
116
+ }
117
+ return null;
118
+ }
119
+
120
+ function inspectActionBinding(schema, expectation) {
121
+ const fieldId = String(expectation.fieldId || '');
122
+ const event = String(expectation.event || 'onChange');
123
+ const actionName = String(expectation.actionName || '');
124
+ const relatedEventId = String(expectation.relatedEventId || '');
125
+ const field = findFieldById(schema && schema.pages || schema, fieldId);
126
+ const eventValue = field && field.props && field.props[event];
127
+ const actualActionName = actionRefName(eventValue);
128
+ const designerBindingFound = isDesignerEventBinding(eventValue, actionName);
129
+ const actions = schema && schema.actions || {};
130
+ const entries = Array.isArray(actions.list) ? actions.list : [];
131
+ const source = actions.module && typeof actions.module.source === 'string'
132
+ ? actions.module.source
133
+ : '';
134
+ const functions = extractExportedActionFunctionNames(source);
135
+ const entry = entries.find(function (item) {
136
+ const itemNames = actionCatalogEntryNames(item);
137
+ const itemEventId = String(item && item.relatedEventId || '');
138
+ return itemNames.includes(actionName) && (!relatedEventId || !itemEventId || itemEventId === relatedEventId);
139
+ });
140
+ const bindingFound = actualActionName === actionName && designerBindingFound;
141
+ const actionEntryFound = !!entry;
142
+ const actionFunctionFound = functions.includes(actionName);
143
+ const mismatches = [];
144
+ if (!field) {mismatches.push('FIELD_NOT_FOUND');}
145
+ if (!actualActionName) {mismatches.push('FIELD_EVENT_BINDING_MISSING');}
146
+ else if (!designerBindingFound) {mismatches.push('DESIGNER_EVENT_BINDING_MISSING');}
147
+ if (!actionEntryFound) {mismatches.push('ACTION_ENTRY_MISSING');}
148
+ if (!actionFunctionFound) {mismatches.push('ACTION_FUNCTION_MISSING');}
149
+
150
+ return {
151
+ fieldId,
152
+ event,
153
+ actionName,
154
+ relatedEventId,
155
+ actualActionName,
156
+ bindingFound,
157
+ designerBindingFound,
158
+ actionEntryFound,
159
+ actionFunctionFound,
160
+ verified: mismatches.length === 0,
161
+ mismatches,
162
+ };
163
+ }
164
+
165
+ function inspectActionBindings(schema, expectations) {
166
+ const bindings = (expectations || []).map(function (expectation) {
167
+ return inspectActionBinding(schema, expectation);
168
+ });
169
+ return {
170
+ verified: bindings.every(function (binding) {return binding.verified;}),
171
+ bindings,
172
+ };
173
+ }
174
+
175
+ module.exports = {
176
+ actionRefs,
177
+ actionRefName,
178
+ buildDesignerEventBinding,
179
+ extractExportedActionFunctionNames,
180
+ findFieldById,
181
+ inspectActionBinding,
182
+ inspectActionBindings,
183
+ isDesignerEventBinding,
184
+ syncDesignerActionCatalog,
185
+ };
@@ -57,7 +57,9 @@ function extractFunctionNames(source) {
57
57
  const names = [];
58
58
  const patterns = [
59
59
  /export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g,
60
+ /export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g,
60
61
  /exports\.([A-Za-z_$][\w$]*)\s*=\s*/g,
62
+ /module\.exports\.([A-Za-z_$][\w$]*)\s*=\s*/g,
61
63
  ];
62
64
  for (const pattern of patterns) {
63
65
  let match;
@@ -94,13 +96,31 @@ function extractFieldMutations(source) {
94
96
  });
95
97
  }
96
98
 
97
- function bindingFromValue(value, path) {
98
- if (!value || typeof value !== 'object' || value.type !== 'actionRef') {return null;}
99
- return {
100
- event: path[path.length - 1] || '',
99
+ function bindingsFromValue(value, path, fieldId, requiresDesignerBinding) {
100
+ if (!value || typeof value !== 'object') {return [];}
101
+ const event = path[path.length - 1] || '';
102
+ if (value.type === 'JSExpression' && Array.isArray(value.events)) {
103
+ const expression = String(value.value || '');
104
+ return value.events.filter(item => item && item.type === 'actionRef').map(item => {
105
+ const actionName = item.name || item.id || '';
106
+ return {
107
+ event,
108
+ actionName,
109
+ fieldId: fieldId || '',
110
+ bindingFormat: 'designerEventFlow',
111
+ designerBindingFound: expression.includes('legaoBuiltin.execEventFlow') &&
112
+ expression.includes('this.' + actionName),
113
+ };
114
+ });
115
+ }
116
+ if (value.type !== 'actionRef') {return [];}
117
+ return [{
118
+ event,
101
119
  actionName: value.name || value.id || '',
102
- fieldId: path.find(part => typeof part === 'string' && /Field_/.test(part)) || '',
103
- };
120
+ fieldId: fieldId || '',
121
+ bindingFormat: 'legacyActionRef',
122
+ designerBindingFound: !requiresDesignerBinding,
123
+ }];
104
124
  }
105
125
 
106
126
  function buildSemanticAnalysis(appType, formUuid, schemaResult, fieldSummary = []) {
@@ -112,18 +132,30 @@ function buildSemanticAnalysis(appType, formUuid, schemaResult, fieldSummary = [
112
132
  const bindings = [];
113
133
  let associationRuleCount = 0;
114
134
 
115
- function visit(value, path = []) {
135
+ function visit(value, path = [], parentFieldId = '', parentRequiresDesignerBinding = false) {
116
136
  if (!value) {return;}
117
137
  if (Array.isArray(value)) {
118
- value.forEach((item, index) => visit(item, path.concat(index)));
138
+ value.forEach((item, index) => visit(
139
+ item,
140
+ path.concat(index),
141
+ parentFieldId,
142
+ parentRequiresDesignerBinding
143
+ ));
119
144
  return;
120
145
  }
121
146
  if (typeof value !== 'object') {return;}
122
147
 
123
- const binding = bindingFromValue(value, path);
124
- if (binding) {bindings.push(binding);}
125
-
126
148
  const props = value.props || {};
149
+ const fieldId = props.fieldId ? String(props.fieldId) : parentFieldId;
150
+ const requiresDesignerBinding = props.fieldId && /Field$/.test(String(value.componentName || ''))
151
+ ? true
152
+ : parentRequiresDesignerBinding;
153
+ const valueBindings = bindingsFromValue(value, path, fieldId, requiresDesignerBinding);
154
+ if (valueBindings.length > 0) {
155
+ bindings.push(...valueBindings);
156
+ return;
157
+ }
158
+
127
159
  if (Array.isArray(props.associationRules)) {
128
160
  associationRuleCount += props.associationRules.length;
129
161
  }
@@ -170,7 +202,7 @@ function buildSemanticAnalysis(appType, formUuid, schemaResult, fieldSummary = [
170
202
 
171
203
  Object.keys(value).forEach((key) => {
172
204
  if (key !== 'source' && key !== 'compiled') {
173
- visit(value[key], path.concat(key));
205
+ visit(value[key], path.concat(key), fieldId, requiresDesignerBinding);
174
206
  }
175
207
  });
176
208
  }
@@ -186,6 +218,16 @@ function buildSemanticAnalysis(appType, formUuid, schemaResult, fieldSummary = [
186
218
  type: item.type || '',
187
219
  relatedEventId: item.relatedEventId || '',
188
220
  }));
221
+ const actionFunctions = extractFunctionNames(source);
222
+ const verifiedBindings = bindings.map(binding => {
223
+ const actionFunctionFound = actionFunctions.includes(binding.actionName);
224
+ const actionEntryFound = actionEntries.some(entry => entry.id === binding.actionName || entry.name === binding.actionName);
225
+ return Object.assign({}, binding, {
226
+ actionFunctionFound,
227
+ actionEntryFound,
228
+ verified: actionFunctionFound && actionEntryFound && binding.designerBindingFound,
229
+ });
230
+ });
189
231
  const mutationFields = unique(extractFieldMutations(source).map(item => item.fieldId));
190
232
 
191
233
  return {
@@ -202,9 +244,9 @@ function buildSemanticAnalysis(appType, formUuid, schemaResult, fieldSummary = [
202
244
  actions: {
203
245
  sourceBytes: Buffer.byteLength(source, 'utf8'),
204
246
  compiledBytes: Buffer.byteLength(compiled, 'utf8'),
205
- functions: extractFunctionNames(source),
247
+ functions: actionFunctions,
206
248
  entries: actionEntries,
207
- bindings,
249
+ bindings: verifiedBindings,
208
250
  urlParams: extractUrlParams(source),
209
251
  fieldMutations: extractFieldMutations(source),
210
252
  referencedMutationFields: mutationFields,
@@ -803,6 +803,12 @@ Examples:
803
803
  patch_must_not_be_empty: 'Patch array must not be empty',
804
804
  patch_invalid_shape: 'Patch must be an array, {operations: []}, or a single operation object',
805
805
  patch_parse_failed: 'Failed to parse patch JSON: ',
806
+ action_event_conflict: 'Field "{0}" event {1} is already bound to action "{2}"; silent replacement is blocked. Set replaceExisting=true only after confirming replacement.',
807
+ action_source_export_missing: 'The action source does not export the requested function "{0}".',
808
+ action_source_missing: 'Action function "{0}" was not found. Provide source/sourceFile or define it in the action module first.',
809
+ action_binding_incomplete: 'The form action function, action registry entry, and field event binding are incomplete. The Schema was not saved.',
810
+ action_readback_failed: 'The form action save was accepted, but readback failed: {0}',
811
+ action_readback_mismatch: 'The form action save was accepted, but the remote function, action registry entry, or field event binding did not match.',
806
812
  rule_file_not_found: 'Rule file not found: ',
807
813
  rule_array_empty: 'Rule array must not be empty',
808
814
  rules_array_empty: 'The rules array must not be empty',
@@ -775,6 +775,12 @@ openyida - 宜搭命令行工具
775
775
  patch_must_not_be_empty: '补丁数组不能为空',
776
776
  patch_invalid_shape: '补丁必须是数组、{operations: []} 或单个操作对象',
777
777
  patch_parse_failed: '补丁 JSON 解析失败: ',
778
+ action_event_conflict: '字段“{0}”的 {1} 已绑定动作“{2}”;默认禁止静默覆盖。确认替换时显式设置 replaceExisting=true。',
779
+ action_source_export_missing: '动作源码未导出指定函数“{0}”。',
780
+ action_source_missing: '未找到动作函数“{0}”;请提供 source/sourceFile,或先在动作模块中定义该函数。',
781
+ action_binding_incomplete: '表单动作函数、动作注册和字段事件绑定不完整,Schema 未保存。',
782
+ action_readback_failed: '表单动作保存已被接受,但回读失败:{0}',
783
+ action_readback_mismatch: '表单动作保存已被接受,但远端回读的函数、动作注册或字段事件绑定不一致。',
778
784
  rule_file_not_found: '规则文件不存在: ',
779
785
  rule_array_empty: '规则数组不能为空',
780
786
  rules_array_empty: 'rules 数组不能为空',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.8.31",
3
+ "version": "2026.9.1",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -4,6 +4,16 @@
4
4
 
5
5
  所有接口返回 Promise,统一使用 `.then()` 和 `.catch()` 处理结果和异常。
6
6
 
7
+ ## 页面 JS 与动作面板基础契约
8
+
9
+ - 只有顶层 `export function actionName(...) {}` 会出现在动作面板,供组件事件和生命周期绑定。
10
+ - 一个导出动作调用另一个导出动作时使用 `this.actionName(...)`;未导出的纯 helper 可直接调用,但不能依赖宜搭注入的 `this`。
11
+ - 表单字段通过 `this.$('fieldId')` 访问,读值用 `getValue()`,赋值用 `setValue(value, options)`;需要触发目标字段 `onChange` 时显式设置 `options.triggerChange`。
12
+ - 组件事件参数以对应组件 API 为准。下拉单选 `onChange` 直接传入动作参数 `value`,不要从 `event` 取值。该参数可能是原始值、`{ value, actionType }`,开启 `useDetailValue=true` 后也可能是 `{ value: { label, value }, actionType }`。兼容写法:先用 `value && value.value !== undefined ? value.value : value` 取得动作值,再以相同方式取得选项明细值。宜搭动作面板不支持空值合并运算符,使用 `?:`。仅写函数源码不算完成,必须在设计器中绑定事件;OpenYida 写入后也必须回读确认绑定仍存在。
13
+ - 不同组件不能统一 `String(value)`:文本、数字、评分、单选和单日期解包后是标量;多选、复选、部门、国家、附件和图片是数组;日期区间是 `{ start, end }`;成员单选交互可能是对象,但初始化可能是数组。只有 `SelectField useDetailValue` 需要继续取选项对象的 `.value`,其他对象或数组必须保留结构。
14
+
15
+ 官方参考:[动作面板](https://alidocs.dingtalk.com/i/nodes/1zknDm0WRz0NZeXQux7OKZXmWBQEx5rG)、[`this` 调用语义](https://alidocs.dingtalk.com/i/nodes/Exel2BLV5gOAdXr1umO9QPNr8gk9rpMq)、[生命周期](https://alidocs.dingtalk.com/i/nodes/pGBa2Lm8aeP35vxdtEKBlz4D8gN7R35y)、[SelectField](https://developers.aliwork.com/docs/components/form/selectField)、[宜搭 JS API](https://developers.aliwork.com/docs/api/yidaAPI)。
16
+
7
17
  ---
8
18
 
9
19
  ## 目录
@@ -36,6 +36,7 @@ description: 宜搭完整应用开发编排技能。对普通 OpenYida 应用做
36
36
  5. **真实 ID 和真实数据**:不编造 `appType`、`formUuid`、`fieldId`、`processCode`、`reportId`。完整应用默认给核心普通表单写入 1-3 条业务化 seed records 并 query 抽查;不适合造数时说明原因和空态方案。
37
37
  6. **自定义页面开发技能固定**:完整应用页面源码按 Step 7 执行。
38
38
  7. **删除必须确认**:用户要求删除应用时,先展示应用名称、应用 ID 和影响范围,等待明确“确认删除”后才能执行。
39
+ 8. **列表页选择**:默认使用普通表单的数据管理页;用户明确要求自定义列表页时才创建 display 页面。
39
40
 
40
41
  ## 关键决策树
41
42
 
@@ -48,7 +49,7 @@ description: 宜搭完整应用开发编排技能。对普通 OpenYida 应用做
48
49
 
49
50
  - 默认页面源码不得使用 `this.dataSourceMap.*`,除非本轮已经创建并绑定对应设计器数据源。
50
51
  - 真实表单数据默认通过页面数据桥或 `window.__OPENYIDA_YIDA_API__.searchFormDatas(params)` 读取;不要用前端 seedRows 冒充真实表单数据。
51
- - 完整应用的列表、看板、详情页优先读取真实表单数据,`page-spec.json` 写 `dataBinding.mode=form`、真实 `appType/formUuid/fieldId` 和字段映射。
52
+ - 用户明确要求的自定义列表、看板和详情页优先读取真实表单数据;`page-spec.json` 写 `dataBinding.mode=form`、真实 `appType/formUuid/fieldId` 和字段映射。表单数据管理页不另生成页面源码。
52
53
  - 完整应用默认先写入 1-3 条业务化 seed records 并 query 抽查;没写入成功时,页面展示空态、表单入口、刷新或登记按钮,并在 final 说明原因。
53
54
  - 若页面确实依赖 `this.dataSourceMap.*`,必须执行 `use_skill("yida-data-source-connectors")` 创建/绑定数据源,并在发布后确认页面 Schema 中存在对应数据源;发布输出出现 `No custom page data sources to preserve` 时,本次发布不能视为完成。
54
55
 
@@ -20,7 +20,7 @@
20
20
  7. 页面、数据、流程或公式确需多字段映射时,对每个目标表单最多一次性执行 `openyida get-schema <appType> <formUuid> --field-map-json`,合并写回 `.cache/<项目名>-schema.json`。
21
21
  8. PRD 包含审批、流程、申请、审核、工单等流程对象时,执行 `use_skill("yida-create-process", "创建带审批流程表单")`。
22
22
  9. 已有流程表单或 `processCode` 时,执行 `use_skill("yida-process-rule", "更新已有流程规则")`。
23
- 10. 分析、复刻或迁移已有表单时,执行 `use_skill("yida-get-schema", "读取字段与行为语义")`,对每个核心表单读取一次 `--analysis-json`;把字段结构与 `actions/fieldBehaviors/associationRuleCount` 分开规划,存在业务动作时使用 `yida-create-form-page` `actions-module`、`bind-field-action`、`bind-datasource` 等高级 patch 能力。
23
+ 10. 分析、复刻或迁移已有表单时,执行 `use_skill("yida-get-schema", "读取字段与行为语义")`,对每个核心表单读取一次 `--analysis-json`;把字段结构与 `actions/fieldBehaviors/associationRuleCount` 分开规划,字段事件动作使用 `yida-create-form-page` 的原子 `field-action`,数据源使用 `bind-datasource`。
24
24
  11. PRD 明确包含原生报表时,执行 `use_skill("yida-report", "按业务统计语义创建原生报表")`;地域分布、日历统计分别使用已支持的 `map`、`calendarHeatmap`,不得无声明退化成柱/饼图。
25
25
  12. PRD 明确包含集成自动化时,执行 `use_skill("yida-integration", "按业务动作创建自动化")`;已有应用先用全类型 `integration list --json` 盘点,创建时区分通知、数据新增/更新、审批完成、定时和手动触发。CLI 不支持的触发类型输出 capability gap,不得用通知替代。
26
26
 
@@ -48,7 +48,7 @@ description: 表单页面创建与更新,默认加载 yida-form-detail 作为
48
48
  |------|------|
49
49
  | 创建新表单 / 设计字段结构 | 本技能 `create` 模式 |
50
50
  | 增删改字段结构 | 本技能 `update` 模式 |
51
- | 配置 OpenYida 尚未封装的平台字段属性/动作 | 本技能 `patch` 模式,先读 [advanced-form-modes.md](references/advanced-form-modes.md) |
51
+ | 配置 OpenYida 尚未封装的平台字段属性/动作 | 本技能 `patch` 模式;字段事件动作使用原子 `field-action`,先读 [advanced-form-modes.md](references/advanced-form-modes.md) |
52
52
  | 字段显示隐藏、只读、自动赋值 | 本技能 `rule` 模式,先读 [advanced-form-modes.md](references/advanced-form-modes.md) |
53
53
  | 选项字段远程搜索数据源 | 本技能 `bind-datasource` 模式,先读 [advanced-form-modes.md](references/advanced-form-modes.md) |
54
54
  | 表单数据记录增删改查 | `yida-data-management` |
@@ -185,7 +185,7 @@ openyida create-form rule <appType> <formUuid> <rulesJsonOrFile>
185
185
 
186
186
  | 模式 | 命令 | 何时使用 |
187
187
  |------|------|------|
188
- | `patch` | `openyida create-form patch <appType> <formUuid> <patchJsonOrFile>` | 受控修改底层 Schema、字段 props、动作模块、自定义校验 |
188
+ | `patch` | `openyida create-form patch <appType> <formUuid> <patchJsonOrFile>` | 受控修改底层 Schema;字段事件动作必须用 `field-action` 并确认 `designerBindingFound: true`、`readbackVerified: true` |
189
189
  | `rule` | `openyida create-form rule <appType> <formUuid> <rulesJsonOrFile>` | 字段显示隐藏、只读、自动赋值、onChange 带出 |
190
190
  | `validation` | `openyida create-form validation <appType> <formUuid> <validationsJsonOrFile>` | 字段校验规则,优先用内置校验,复杂场景再用 customValidate |
191
191
  | `bind-datasource` | `openyida create-form bind-datasource <appType> <formUuid> <fieldLabelOrId> <dataSourceJsonOrFile>` | 选项字段绑定远程搜索数据源;成功输出 `resolved` |
@@ -19,8 +19,41 @@ openyida create-form patch <appType> <formUuid> <patchJsonOrFile>
19
19
  | `merge` | 对指定 JSON Pointer 路径做对象深合并 |
20
20
  | `actions-module` | 写入页面动作模块 `source` / `compiled`,`source` 会自动编译 |
21
21
  | `bind-field-action` | 给字段事件(如 `onChange`)绑定动作引用 |
22
+ | `field-action` | 原子写入动作函数、注册动作并绑定字段事件;字段自定义事件默认使用此操作 |
22
23
  | `bind-datasource` | 给选项类字段绑定远程搜索数据源(高阶入口优先用 bind-datasource 模式) |
23
24
 
25
+ 字段事件动作不得只写 `actions-module`。默认使用 `field-action`,并以返回的 `designerBindingFound: true` 和 `readbackVerified: true` 为完成条件:
26
+
27
+ ```json
28
+ [
29
+ {
30
+ "action": "field-action",
31
+ "field": "状态",
32
+ "event": "onChange",
33
+ "name": "handleStatusChange",
34
+ "source": "export function handleStatusChange(value) {\n var actionValue = value && value.value !== undefined ? value.value : value;\n var selectedValue = actionValue && actionValue.value !== undefined ? actionValue.value : actionValue;\n if (selectedValue === 'A') {\n this.$('textField_result').setValue('已执行');\n }\n}"
35
+ }
36
+ ]
37
+ ```
38
+
39
+ 入口动作必须是顶层 `export function`,否则不会出现在宜搭动作面板。导出动作之间通过 `this.xxx()` 调用;未导出的纯 helper 可直接调用,但不能使用宜搭页面上下文。下拉单选 `onChange` 会把动作参数作为 `value` 传入,不要从 `event` 取值。该参数可能是原始值、`{ value, actionType }`,开启 `useDetailValue=true` 后也可能是 `{ value: { label, value }, actionType }`;先取动作值,再取选项明细值即可兼容三种形态。宜搭动作面板不支持空值合并运算符,使用 `?:`。
40
+
41
+ 不同组件不能统一 `String(value)`。先用 `value && value.value !== undefined ? value.value : value` 去掉动作参数外层,再按组件处理:
42
+
43
+ | 组件 | 实际动作值 |
44
+ |------|------------|
45
+ | TextField / NumberField / RateField / RadioField | 字符串或数字 |
46
+ | DateField | 毫秒时间戳 |
47
+ | MultiSelectField / CheckboxField | 选中值数组 |
48
+ | DepartmentSelectField / CountrySelectField | `{ text, value }` 数组 |
49
+ | AttachmentField / ImageField | 文件对象数组 |
50
+ | CascadeDateField | `{ start, end }` |
51
+ | EmployeeField | 单选交互可能是成员对象,初始化可能是成员数组,先归一化为数组 |
52
+
53
+ `SelectField` 开启 `useDetailValue` 后再取一次 `.value`;其他对象或数组必须保留结构,不能盲目取第二层。
54
+
55
+ 如果目标事件已有其他动作,命令默认停止,避免静默覆盖;只有确认替换时才设置 `replaceExisting: true`。`actions-module` + `bind-field-action` 仅保留给低阶迁移场景,仍会执行设计器原生绑定校验和保存后回读。运行时发生联动但设计器仍显示“新建动作”属于失败,不能作为验收通过。
56
+
24
57
  隐藏但仍提交字段:
25
58
 
26
59
  ```json
@@ -66,7 +99,7 @@ openyida create-form patch <appType> <formUuid> <patchJsonOrFile>
66
99
  - 函数返回 `true` 表示校验通过,`false` 表示校验失败
67
100
  - 子表内字段通过 `this.item.values` 获取同行其他字段的值
68
101
 
69
- 如必须桥接 JS 面板函数,先用 `actions-module` 写入函数,再在字段自定义函数里通过 `this.xxx()` 调用。
102
+ 如必须桥接 JS 面板函数,使用 `field-action` 原子写入并绑定入口函数,再在字段自定义函数里通过 `this.xxx()` 调用。
70
103
 
71
104
  ## rule 模式(字段联动与自动赋值)
72
105
 
@@ -122,7 +155,7 @@ JS 表达式计算目标值,表达式可使用 `value`(触发字段值)和
122
155
  ]
123
156
  ```
124
157
 
125
- rule 模式会自动生成宜搭动作代码,绑定触发字段的 `onChange`,并在页面加载/表单数据初始化后执行一次规则。若字段已有 `onChange` 动作,OpenYida 会先调用原动作,再执行生成的规则。
158
+ rule 模式会自动生成宜搭动作代码,绑定触发字段的 `onChange`,并在页面加载/表单数据初始化后执行一次规则。每次调用传入的规则数组视为当前 OpenYida 联动规则全集;重写时会清理不再使用的生成绑定。若字段已有 `onChange` 动作,OpenYida 会保留并先调用原动作,再执行生成的规则。
126
159
 
127
160
  ## bind-datasource 模式(选项字段远程搜索数据源)
128
161
 
@@ -48,7 +48,7 @@ description: >
48
48
 
49
49
  1. **平台能力优先**:数据录入、提交、编辑、审批、权限、字段校验走宜搭表单/流程;自定义页负责展示数据、呈现分析结果、放置业务入口、打开详情页,并串联表单、流程、报表和导航入口。
50
50
  2. **需求分析归本技能**:完整应用设计先写清应用基本信息、用户角色、核心任务、业务对象、数据结构、页面与表单/流程资源、业务逻辑、交互状态和验收标准。
51
- 3. **应用资源蓝图先行**:完整应用设计先列清资源蓝图,包括主页面/工作台/看板/列表/详情等 display 自定义页面,以及普通表单、流程表单和报表;表单字段写业务语义、字段类型、必填、默认值、关联关系和分组,运行后 ID 由实现阶段记录。
51
+ 3. **应用资源蓝图先行**:资源蓝图列出必要的 display 页面、表单、流程和报表。普通表单的数据管理页默认作为列表;用户明确要求时才规划自定义列表页。表单写业务语义、类型、必填、默认值、关系和分组;运行 ID 由实现阶段记录。
52
52
  4. **顺序分开写清**:PRD 同时写资源创建顺序、页面实现交付顺序和导航顺序。资源创建顺序服务依赖关系,表单/流程在自定义页面之前;页面实现交付顺序服务开发验收;导航顺序服务用户入口展示。
53
53
  5. **美感提升保持功能契约**:页面美化、视觉升级和页面重构默认只调整颜色、布局、密度、间距、视觉层级、素材和图标表达;现有数据源、字段映射、按钮动作、筛选逻辑、提交 URL、权限和业务状态保持原样。
54
54
  6. **默认保留平台应用导航**:普通自定义页、页面内 tab、分段、筛选和快捷入口都不触发 `yida-nav-shell`。只有自定义页要做顶部导航、侧边导航、导航壳、自绘应用级导航,或用户明确隐藏应用导航时,才写 `appBlueprint.hideAppNav: 'y'` 并交给 `yida-nav-shell`。用户只说全屏、无导航或 `isRenderNav=false` 时,只写页面级隐藏配置。
@@ -121,10 +121,12 @@
121
121
  | 资源 | 类型 | 用途 | 关键字段 / 功能 | 创建策略 |
122
122
  | --- | --- | --- | --- | --- |
123
123
  | <主页面> | display-page / main | <入口和概览> | <contentBlocks:推荐 8-10 个区块以上,如标题上下文、筛选、摘要、主操作、待办、最近记录、动态、提醒、右侧上下文、空态行动等;不作为硬门槛> | <复用 / 创建> |
124
- | <业务表单> | normal-form | <数据录入和管理> | <核心字段> | <复用 / 创建 / 更新> |
124
+ | <业务表单> | normal-form | <数据录入和列表管理> | <核心字段> | <复用 / 创建 / 更新> |
125
125
  | <审批表单> | process-form | <流程闭环> | <节点和条件> | <复用 / 创建 / 更新> |
126
126
  | <报表> | report | <汇总分析> | <指标口径> | <复用 / 创建 / 更新> |
127
127
 
128
+ 默认不创建自定义列表页;用户明确要求时才新增 `display-page / list`。
129
+
128
130
  ## 8. 资源创建顺序
129
131
 
130
132
  资源创建顺序服务于依赖关系;表单/流程在自定义页面之前创建,便于页面消费表单 URL、字段语义和数据来源。
@@ -146,7 +148,7 @@
146
148
  | 顺序 | 页面 | 实现重点 | 依赖资源 | 验收点 |
147
149
  | --- | --- | --- | --- | --- |
148
150
  | 1 | <主页面 / 工作台 / 官网首页> | <首屏、核心入口、主题风格> | <表单入口 / 空态 / 指标口径> | <打开后能完成核心判断> |
149
- | 2 | <核心列表 / 管理页> | <筛选、表格、详情抽屉、主操作> | <核心业务表单> | <能查看或进入新增/详情> |
151
+ | 2 | <数据管理入口> | <默认使用表单数据管理页;用户要求时实现自定义列表> | <核心业务表单> | <可新增或查看详情> |
150
152
  | 3 | <详情 / 看板 / 大屏> | <对象叙事、图表、状态、洞察> | <表单数据 / 报表> | <能解释业务状态> |
151
153
  | 4 | <辅助页面> | <配置、说明、低频入口> | <相关表单或链接> | <不阻断主链路> |
152
154
 
@@ -158,7 +160,7 @@
158
160
  | --- | --- | --- | --- |
159
161
  | 门户 / 首页 | <主页面 / 工作台 / 官网首页> | <平台导航 / 顶部导航 / 侧边导航 / 单页入口> | 第一入口放最前 |
160
162
  | 业务办理 | <流程表单 / 新增入口 / 待办相关页面> | <平台导航或页面内快捷入口> | 高频动作靠前 |
161
- | 数据管理 | <普通表单 / 列表页 / 详情页> | <平台导航分组> | 数据录入、查询和维护集中 |
163
+ | 数据管理 | <表单数据管理页 / 用户要求的自定义列表页 / 详情页> | <平台导航分组> | 数据录入、查询和维护集中 |
162
164
  | 经营分析 | <看板 / 报表 / 大屏> | <平台导航 / 大屏全屏入口 / 页面级隐藏导航 isRenderNav=false> | 管理者查看,放在业务操作之后或独立分组 |
163
165
  | 系统配置 | <配置表 / 字典表 / 权限说明> | <平台导航靠后分组> | 低频维护靠后 |
164
166
 
@@ -170,7 +172,7 @@
170
172
  | 数据录入 | <表单能提交,提交后页面能刷新或回到正确入口> |
171
173
  | 表单主题和详情页样式 | <普通表单和流程表单已注入 `style#yida-global-theme`,formDetail 详情页已注入 `style#yida-form-detail-style`,与应用和自定义页面主题色一致> |
172
174
  | 初始示例数据 | <完整应用默认已为核心普通表单写入 1-3 条业务化示例记录并 query 抽查;跳过时说明原因> |
173
- | 数据查看 | <列表 / 看板 / 详情能看到真实表单数据或清晰空态> |
175
+ | 数据查看 | <默认使用表单数据管理页;自定义列表 / 看板 / 详情显示真实数据或空态> |
174
176
  | 权限 / 流程 | <权限规则或流程节点生效> |
175
177
  | 视觉一致性 | <主题色、布局密度、状态、图标和当前业务一致> |
176
178
  | 导航可用 | <导航顺序符合 PRD,主入口靠前,配置/低频资源靠后> |
@@ -18,7 +18,8 @@
18
18
  | 页面 | resourceType | scene | 用途 | 实现链路 |
19
19
  | --- | --- | --- | --- | --- |
20
20
  | 主页 / 首页 / 工作台 | `display-page` | `workbench/dashboard/landing` | 应用第一入口、指标概览、快捷入口 | 自定义页面 |
21
- | 管理列表页 | `display-page` | `list` | 查询、筛选、批量操作、详情入口 | 自定义页面 |
21
+ | 表单数据管理页 | `normal-form` | `native-list` | 查询、筛选、查看、编辑和维护 | 宜搭表单数据管理页(默认) |
22
+ | 自定义列表页 | `display-page` | `list` | 用户明确要求自定义列表页 | 自定义页面 |
22
23
  | 详情页 | `display-page` | `detail` | 单对象信息总览、时间线、关联对象 | 自定义页面 |
23
24
  | 数据大屏 / 看板 | `display-page` 或报表 | `screen/dashboard` | 指标监控、经营分析、投屏展示 | Canvas / Recharts / 报表 |
24
25
 
@@ -31,6 +32,8 @@
31
32
 
32
33
  资源清单使用业务语义和资源类型;`appType/corpId/baseUrl` 写入 PRD 的应用配置,`formUuid`、`fieldId`、`processCode` 等细节 ID 由实现阶段写入 `.cache/<项目名>-schema.json`。
33
34
 
35
+ 同一业务对象默认使用普通表单的数据管理页;用户明确要求时才增加自定义列表页。
36
+
34
37
  ## 给页面标场景
35
38
 
36
39
  页面 `scene` 只作为分类标签和实现提示,不作为固定页面样式。页面结构必须来自当前业务目标、用户任务、资源关系和后续 `design.md`。
@@ -40,7 +43,7 @@
40
43
  | workbench | 进入应用后处理任务、看状态、做高频动作 | 当前业务 `contentBlocks` + `design.md.visualScaffold` |
41
44
  | dashboard | 经营分析、指标判断、趋势和排行 | 指标口径 + 图表目的 + `design.md.visualScaffold` |
42
45
  | screen | 投屏、监控、态势感知 | 实时信息层级 + 大屏展示目标 + `design.md.visualScaffold` |
43
- | list | 查询、筛选、批量操作、详情入口 | 数据字段、筛选条件、操作路径 + `design.md.visualScaffold` |
46
+ | list | 用户明确要求的自定义列表 | 数据字段、筛选、操作路径 + `design.md.visualScaffold` |
44
47
  | detail | 单对象总览、时间线、关联对象 | 对象信息架构 + 关联关系 + `design.md.visualScaffold` |
45
48
  | landing | 对外介绍、品牌表达、价值转化 | 价值路径、素材清单、CTA + `design.md.visualScaffold` |
46
49
  | split-pane | 左列表右详情、处理台 | 主从关系、处理路径 + `design.md.visualScaffold` |