diff --git a/packages/core/client/src/flow/models/blocks/form/FormBlockModel.tsx b/packages/core/client/src/flow/models/blocks/form/FormBlockModel.tsx index 1a5392ecec8..8f9fcb97f59 100644 --- a/packages/core/client/src/flow/models/blocks/form/FormBlockModel.tsx +++ b/packages/core/client/src/flow/models/blocks/form/FormBlockModel.tsx @@ -18,6 +18,7 @@ import { } from '@nocobase/flow-engine'; import { Form, FormInstance } from 'antd'; import { omit } from 'lodash'; +import { getValuesByPath } from '@nocobase/shared'; import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { commonConditionHandler, ConditionBuilder } from '../../../components/ConditionBuilder'; import { @@ -306,6 +307,18 @@ export class FormBlockModel< const topValue = this.form?.getFieldValue?.(top); if (topValue == null) return false; if (Array.isArray(topValue) && topValue.length === 0) return false; + + // 本地优先:支持对多关系的 dot 聚合路径(例如 assignees.name)。 + // lodash.get 对数组聚合路径会返回 undefined(如 _.get({ assignees:[{name:'A'}] }, 'assignees.name')), + // 因而这里先用 getValuesByPath 做一次前端可解析性检查,命中则直接前端解析。 + const formValuesSnapshot = runtime.getFormValuesSnapshot(); + if (formValuesSnapshot && typeof formValuesSnapshot === 'object') { + const localResolved = getValuesByPath(formValuesSnapshot as Record, subPath); + if (typeof localResolved !== 'undefined') { + return false; + } + } + // 已配置字段:仅关联字段的子路径按需服务端补全(保持现有语义) const assocResolver = createAssociationSubpathResolver( () => this.collection, diff --git a/packages/core/client/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx b/packages/core/client/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx index 52c63058176..015ad2e6b1f 100644 --- a/packages/core/client/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +++ b/packages/core/client/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx @@ -435,6 +435,48 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)', expect(api.request).toHaveBeenCalledTimes(1); }); + it('configured toMany dot aggregation path uses local value and skips server', async () => { + const model = await setupFormModel(); + + const api = { + request: vi.fn(async () => { + return { data: { ok: true } } as any; + }), + } as any; + (model.flowEngine.context as any).defineProperty('api', { value: api }); + + function HookCaller() { + model.useHooksBeforeRender(); + return null; + } + render(React.createElement(HookCaller)); + + const mem: Record = {}; + const fakeForm = { + setFieldsValue: (v: any) => Object.assign(mem, v), + getFieldsValue: () => ({ ...mem }), + getFieldValue: (namePath: any) => getByPath(mem, namePath), + setFieldValue: (k: string, v: any) => (mem[k] = v), + }; + (model.context as any).defineProperty('form', { value: fakeForm }); + fakeForm.setFieldsValue({ + assignees: [ + { id: 3, name: 'A' }, + { id: 5, name: 'B' }, + ], + }); + mockFormGridEnabledFields(model, ['assignees']); + + const tpl = { names: '{{ ctx.formValues.assignees.name }}' } as any; + const out = await (model.context as any).resolveJsonTemplate(tpl); + expect(out).toEqual({ names: ['A', 'B'] }); + expect(api.request).toHaveBeenCalledTimes(0); + + const names = await (model.context as any).getVar('ctx.formValues.assignees.name'); + expect(names).toEqual(['A', 'B']); + expect(api.request).toHaveBeenCalledTimes(0); + }); + it('unconfigured field: new record returns undefined and does not call server', async () => { const model = await setupFormModel();