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.
- package/lib/app/create-app.js +1 -0
- package/lib/app/create-form.js +334 -66
- package/lib/app/form-action-binding.js +185 -0
- package/lib/app/schema-semantic-analysis.js +56 -14
- package/lib/core/locales/en.js +6 -0
- package/lib/core/locales/zh.js +6 -0
- package/package.json +1 -1
- package/yida-skills/references/yida-api.md +10 -0
- package/yida-skills/skills/yida-app/SKILL.md +2 -1
- package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +1 -1
- package/yida-skills/skills/yida-create-form-page/SKILL.md +2 -2
- package/yida-skills/skills/yida-create-form-page/references/advanced-form-modes.md +35 -2
- package/yida-skills/skills/yida-design/SKILL.md +1 -1
- package/yida-skills/skills/yida-design/workflow/output-prd.md +6 -4
- package/yida-skills/skills/yida-design/workflow/step-3-information-architecture.md +5 -2
package/lib/app/create-app.js
CHANGED
|
@@ -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(),
|
package/lib/app/create-form.js
CHANGED
|
@@ -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
|
-
|
|
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 + '
|
|
3393
|
+
? ' if (typeof this.' + binding.previousActionName + ' === "function") {\n this.' + binding.previousActionName + '(event);\n }\n'
|
|
3255
3394
|
: '';
|
|
3256
|
-
|
|
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
|
|
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
|
|
3548
|
+
if (typeof this.didMount === 'function') {
|
|
3549
|
+
this.didMount(event);
|
|
3410
3550
|
}
|
|
3411
|
-
return openyidaApplyRules
|
|
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
|
|
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
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
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
|
-
{
|
|
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
|
|
98
|
-
if (!value || typeof value !== 'object'
|
|
99
|
-
|
|
100
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
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,
|
package/lib/core/locales/en.js
CHANGED
|
@@ -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',
|
package/lib/core/locales/zh.js
CHANGED
|
@@ -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
|
@@ -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
|
-
-
|
|
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`
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
|
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 |
|
|
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` |
|