From ab12002d11c72479d1548d7414ddb43af323cfa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=AA=E5=BF=86=E5=A4=A9=E5=A0=82?= <1455668754@qq.com> Date: Tue, 1 Sep 2026 17:58:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(form-ui):=20=E5=8A=A8=E6=80=81=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E5=AE=9A=E4=B9=89=E4=B8=8B=E9=87=8D=E7=BD=AE=E6=97=B6?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E9=94=99=E8=AF=AF=E7=9A=84=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复:表单定义动态变化后(如抽屉中先编辑再新增),重置表单会恢复到挂载时固化的旧默认值,部分字段初始值丢失 - 调整:重置未显式指定值时,按当前表单定义动态计算默认值,并同步校正底层默认值快照 - 重构:表单定义默认值计算逻辑抽取为公共函数,挂载初始化与重置共用 --- packages/@core/ui-kit/form-ui/CHANGELOG.md | 3 + .../ui-kit/form-ui/__tests__/form-api.test.ts | 36 +++++++++ packages/@core/ui-kit/form-ui/src/form-api.ts | 13 ++++ .../ui-kit/form-ui/src/form-default-values.ts | 73 +++++++++++++++++++ .../ui-kit/form-ui/src/use-form-context.ts | 58 +-------------- 5 files changed, 128 insertions(+), 55 deletions(-) create mode 100644 packages/@core/ui-kit/form-ui/src/form-default-values.ts diff --git a/packages/@core/ui-kit/form-ui/CHANGELOG.md b/packages/@core/ui-kit/form-ui/CHANGELOG.md index 2f0aba74c..b9fc4705e 100644 --- a/packages/@core/ui-kit/form-ui/CHANGELOG.md +++ b/packages/@core/ui-kit/form-ui/CHANGELOG.md @@ -4,6 +4,9 @@ ### Patch Changes +- fix(@vben-core/form-ui): 修复动态表单定义下重置时恢复到挂载时固化的旧默认值的问题。表单定义动态变化后(如抽屉中先编辑再新增),部分依赖定义默认值的字段初始值会丢失;现在 `reset()` 未显式指定重置值时,会按当前表单定义动态计算默认值,并同步校正底层默认值快照 +- refactor(@vben-core/form-ui): 将表单定义默认值的计算逻辑抽取为公共函数,挂载初始化与重置共用 + - [#7978](https://github.com/vbenjs/vue-vben-admin/pull/7978) [`9ffd42f`](https://github.com/vbenjs/vue-vben-admin/commit/9ffd42f013825f94278165027bc210a5314d3998) Thanks [@SaleriHQ](https://github.com/SaleriHQ)! - feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray - Updated dependencies [[`142b544`](https://github.com/vbenjs/vue-vben-admin/commit/142b5442c2270090720a92671a0573cfe6974fa3)]: diff --git a/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts b/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts index e215caf6b..19578d56c 100644 --- a/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts +++ b/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts @@ -50,6 +50,42 @@ describe('formApi', () => { expect(formApi.getFieldComponentRef('name')).toBeUndefined(); }); + it('should reset to current schema default values', async () => { + const reset = vi.fn(); + const formActions: any = { + meta: {}, + reset, + values: {}, + }; + await formApi.mount(formActions, new Map()); + + // 动态切换表单定义后重置,应按当前定义的默认值恢复(而非挂载时固化的旧默认值) + formApi.setState({ + schema: [ + { component: 'input', defaultValue: '默认名称', fieldName: 'name' }, + ], + }); + await formApi.reset(); + expect(reset).toHaveBeenCalledWith( + { values: { name: '默认名称' } }, + { force: true }, + ); + + // 未声明默认值的定义重置为空值 + formApi.setState({ + schema: [{ component: 'input', fieldName: 'remark' }], + }); + await formApi.reset(); + expect(reset).toHaveBeenLastCalledWith({ values: {} }, { force: true }); + + // 显式指定重置值时按调用方参数原样执行 + await formApi.reset({ values: { name: '自定义' } }); + expect(reset).toHaveBeenLastCalledWith( + { values: { name: '自定义' } }, + undefined, + ); + }); + it('should get values from form', async () => { const formActions: any = { meta: {}, diff --git a/packages/@core/ui-kit/form-ui/src/form-api.ts b/packages/@core/ui-kit/form-ui/src/form-api.ts index 889556616..b19b22ed4 100644 --- a/packages/@core/ui-kit/form-ui/src/form-api.ts +++ b/packages/@core/ui-kit/form-ui/src/form-api.ts @@ -28,6 +28,7 @@ import { import { warnDeprecatedOnce } from './deprecation'; import { resolveFieldNamePath } from './field-name'; import { decodeFormValues, encodeFormValues } from './form-codec'; +import { generateSchemaDefaultValues } from './form-default-values'; import { updateFormSchemaList } from './form-render/schema'; import { formatFormValues } from './form-value-transform'; @@ -374,6 +375,18 @@ export class FormApi< */ async reset(state?: FormResetState, opts?: FormResetOptions) { const form = await this.getForm(); + // 未显式指定重置值时,按当前表单定义重新计算默认值, + // 避免动态定义下恢复到挂载时固化的旧默认值 + if (!state?.values) { + return form.reset( + { + values: generateSchemaDefaultValues( + this.state?.schema ?? [], + ) as Partial, + }, + { ...opts, force: true }, + ); + } return form.reset(state, opts); } diff --git a/packages/@core/ui-kit/form-ui/src/form-default-values.ts b/packages/@core/ui-kit/form-ui/src/form-default-values.ts new file mode 100644 index 000000000..926215fbb --- /dev/null +++ b/packages/@core/ui-kit/form-ui/src/form-default-values.ts @@ -0,0 +1,73 @@ +import type { ZodType } from 'zod'; + +import type { FormSchemaRuleType } from './types'; + +import { toRaw } from 'vue'; + +import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils'; + +import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod'; +import { getDefaultsForSchema } from 'zod-defaults'; + +/** 仅依赖计算默认值所需的最小字段结构,兼容任意泛型表单定义 */ +interface SchemaLike { + defaultValue?: any; + fieldName: string; + rules?: FormSchemaRuleType; +} + +/** + * 根据表单定义计算默认值。 + * + * 优先取字段显式声明的默认值;未声明时,尝试从校验规则中推断。 + * 动态表单在定义变化后需重新计算,否则重置时会恢复到挂载时的旧默认值 + */ +export function generateSchemaDefaultValues( + schema: readonly SchemaLike[] = [], +): Record { + const initialValues: Record = {}; + + const zodObject: Record = {}; + (schema || []).forEach((item) => { + if (Reflect.has(item, 'defaultValue')) { + set(initialValues, item.fieldName, item.defaultValue); + } else if (item.rules && !isString(item.rules)) { + // 检查规则是否适合提取默认值 + const rawRules = toRaw(item.rules); + const customDefaultValue = getCustomDefaultValue(rawRules); + zodObject[item.fieldName] = rawRules; + if (customDefaultValue !== undefined) { + initialValues[item.fieldName] = customDefaultValue; + } + } + }); + + const schemaInitialValues = getDefaultsForSchema(object(zodObject)); + + const zodDefaults: Record = {}; + for (const key in schemaInitialValues) { + set(zodDefaults, key, schemaInitialValues[key]); + } + return mergeWithArrayOverride(initialValues, zodDefaults); +} + +/** 从校验规则中推断默认值 */ +function getCustomDefaultValue(rule: any): any { + rule = toRaw(rule); + if (rule instanceof ZodString) { + return ''; // 默认为空字符串 + } else if (rule instanceof ZodNumber) { + return null; // 默认为 null(避免显示 0) + } else if (rule instanceof ZodObject) { + // 递归提取嵌套对象的默认值 + const defaultValues: Record = {}; + for (const [key, valueSchema] of Object.entries(rule.shape)) { + defaultValues[key] = getCustomDefaultValue(valueSchema); + } + return defaultValues; + } else if (rule instanceof ZodIntersection) { + return getDefaultsForSchema(rule); + } else { + return undefined; // 其他类型不提供默认值 + } +} diff --git a/packages/@core/ui-kit/form-ui/src/use-form-context.ts b/packages/@core/ui-kit/form-ui/src/use-form-context.ts index ae3d69f90..a0b6a16a6 100644 --- a/packages/@core/ui-kit/form-ui/src/use-form-context.ts +++ b/packages/@core/ui-kit/form-ui/src/use-form-context.ts @@ -1,17 +1,12 @@ -import type { ZodType } from 'zod'; - import type { ComputedRef } from 'vue'; import type { ExtendedFormApi, FormActions, VbenFormProps } from './types'; -import { computed, toRaw, unref, useSlots } from 'vue'; +import { computed, unref, useSlots } from 'vue'; import { createContext } from '@vben-core/shadcn-ui'; -import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils'; - -import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod'; -import { getDefaultsForSchema } from 'zod-defaults'; +import { generateSchemaDefaultValues } from './form-default-values'; import { useFormRuntime } from './form-runtime'; type ExtendFormProps = VbenFormProps & { @@ -30,7 +25,7 @@ export function useFormInitial( props: ComputedRef | VbenFormProps, ) { const slots = useSlots(); - const initialValues = generateInitialValues(); + const initialValues = generateSchemaDefaultValues(unref(props).schema); const form = useFormRuntime(initialValues); @@ -45,53 +40,6 @@ export function useFormInitial( return resultSlots; }); - function generateInitialValues() { - const initialValues: Record = {}; - - const zodObject: Record = {}; - (unref(props).schema || []).forEach((item) => { - if (Reflect.has(item, 'defaultValue')) { - set(initialValues, item.fieldName, item.defaultValue); - } else if (item.rules && !isString(item.rules)) { - // 检查规则是否适合提取默认值 - const rawRules = toRaw(item.rules); - const customDefaultValue = getCustomDefaultValue(rawRules); - zodObject[item.fieldName] = rawRules; - if (customDefaultValue !== undefined) { - initialValues[item.fieldName] = customDefaultValue; - } - } - }); - - const schemaInitialValues = getDefaultsForSchema(object(zodObject)); - - const zodDefaults: Record = {}; - for (const key in schemaInitialValues) { - set(zodDefaults, key, schemaInitialValues[key]); - } - return mergeWithArrayOverride(initialValues, zodDefaults); - } - // 自定义默认值提取逻辑 - function getCustomDefaultValue(rule: any): any { - rule = toRaw(rule); - if (rule instanceof ZodString) { - return ''; // 默认为空字符串 - } else if (rule instanceof ZodNumber) { - return null; // 默认为 null(避免显示 0) - } else if (rule instanceof ZodObject) { - // 递归提取嵌套对象的默认值 - const defaultValues: Record = {}; - for (const [key, valueSchema] of Object.entries(rule.shape)) { - defaultValues[key] = getCustomDefaultValue(valueSchema); - } - return defaultValues; - } else if (rule instanceof ZodIntersection) { - return getDefaultsForSchema(rule); - } else { - return undefined; // 其他类型不提供默认值 - } - } - return { delegatedSlots, form,