From 98dea92225ee53006321ad451276e3b6db2194e9 Mon Sep 17 00:00:00 2001 From: dream-weave <62940878+dream-weave@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:09:31 +0800 Subject: [PATCH 1/2] feat(@vben-core/form-ui): add typed form value codecs (#8189) * feat(@vben-core/form-ui): add typed form value codecs * test(@vben-core/form-ui): cover codec value boundaries * refactor(project): propagate form codec generics * feat(@vben/plugins): propagate form codecs through vxe grids * refactor(@vben/playground): migrate form codec examples * refactor(@vben/playground): migrate query forms to codecs * docs(@vben/docs): document form value codecs --- apps/web-antd/src/adapter/form.ts | 25 ++- apps/web-antd/src/adapter/vxe-table.ts | 22 +- apps/web-antdv-next/src/adapter/form.ts | 25 ++- apps/web-antdv-next/src/adapter/vxe-table.ts | 22 +- apps/web-ele/src/adapter/form.ts | 25 ++- apps/web-ele/src/adapter/vxe-table.ts | 22 +- apps/web-naive/src/adapter/form.ts | 25 ++- apps/web-naive/src/adapter/vxe-table.ts | 22 +- apps/web-tdesign/src/adapter/form.ts | 25 ++- apps/web-tdesign/src/adapter/vxe-table.ts | 22 +- docs/src/_env/adapter/form.ts | 28 ++- docs/src/components/common-ui/vben-form.md | 87 ++++---- .../demos/vben-form/value-format/index.vue | 143 +++++++----- docs/src/en/components/common-ui/vben-form.md | 37 +++- .../guide/in-depth/zod-v4-form-migration.md | 14 +- .../guide/in-depth/zod-v4-form-migration.md | 14 +- .../ui-kit/form-ui/__tests__/form-api.test.ts | 95 ++++++++ .../form-ui/__tests__/form-codec.test.ts | 81 +++++++ .../__tests__/form-compatibility.test.ts | 79 +++++++ .../__tests__/form-integration.test.ts | 177 +++++++++++++++ .../form-ui/__tests__/form-types.test.ts | 97 ++++++++ packages/@core/ui-kit/form-ui/src/form-api.ts | 208 ++++++++++++++---- .../@core/ui-kit/form-ui/src/form-codec.ts | 40 ++++ .../ui-kit/form-ui/src/form-render/form.vue | 2 +- .../@core/ui-kit/form-ui/src/form-runtime.ts | 22 +- packages/@core/ui-kit/form-ui/src/index.ts | 5 + packages/@core/ui-kit/form-ui/src/types.ts | 107 ++++++--- .../@core/ui-kit/form-ui/src/use-vben-form.ts | 26 ++- .../ui-kit/form-ui/src/vben-use-form.vue | 2 +- packages/effects/plugins/src/vxe-table/api.ts | 46 +++- .../effects/plugins/src/vxe-table/types.ts | 20 +- .../plugins/src/vxe-table/use-vxe-grid.ts | 26 ++- playground/src/adapter/form.ts | 25 ++- playground/src/adapter/vxe-table.ts | 23 +- .../src/views/demos/form-array/README.md | 41 ++-- .../src/views/demos/form-array/index.vue | 59 ++++- playground/src/views/examples/form/basic.vue | 40 +++- playground/src/views/examples/form/custom.vue | 36 ++- .../src/views/examples/form/value-format.vue | 101 ++++++--- .../src/views/examples/vxe-table/form.vue | 40 +++- .../src/views/system/menu/modules/form.vue | 61 +++-- playground/src/views/system/role/list.vue | 37 +++- playground/src/views/system/user/list.vue | 37 +++- 43 files changed, 1678 insertions(+), 413 deletions(-) create mode 100644 packages/@core/ui-kit/form-ui/__tests__/form-codec.test.ts create mode 100644 packages/@core/ui-kit/form-ui/src/form-codec.ts diff --git a/apps/web-antd/src/adapter/form.ts b/apps/web-antd/src/adapter/form.ts index c939b726..67aae156 100644 --- a/apps/web-antd/src/adapter/form.ts +++ b/apps/web-antd/src/adapter/form.ts @@ -42,18 +42,27 @@ async function initSetupVbenForm() { }); } -function useVbenForm( - options: FormProps, +function useVbenForm< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + options: FormProps< + ComponentType, + ComponentPropsMap, + TFormValues, + TSubmitValues + >, ) { - return useForm(options); + return useForm( + options, + ); } export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; -export type VbenFormProps = FormProps< - ComponentType, - ComponentPropsMap, - TValues ->; +export type VbenFormProps< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +> = FormProps; diff --git a/apps/web-antd/src/adapter/vxe-table.ts b/apps/web-antd/src/adapter/vxe-table.ts index 7dd273f5..75b56375 100644 --- a/apps/web-antd/src/adapter/vxe-table.ts +++ b/apps/web-antd/src/adapter/vxe-table.ts @@ -1,3 +1,4 @@ +import type { FormValues } from '@vben/common-ui'; import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; import type { ComponentPropsMap, ComponentType } from './component'; @@ -70,8 +71,23 @@ setupVbenVxeTable({ useVbenForm, }); -export const useVbenVxeGrid = >( - ...rest: Parameters> -) => useGrid(...rest); +export const useVbenVxeGrid = < + T extends Record, + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + ...rest: Parameters< + typeof useGrid< + T, + ComponentType, + ComponentPropsMap, + TFormValues, + TSubmitValues + > + > +) => + useGrid( + ...rest, + ); export type * from '@vben/plugins/vxe-table'; diff --git a/apps/web-antdv-next/src/adapter/form.ts b/apps/web-antdv-next/src/adapter/form.ts index 5ff63809..9bbfd5c1 100644 --- a/apps/web-antdv-next/src/adapter/form.ts +++ b/apps/web-antdv-next/src/adapter/form.ts @@ -41,18 +41,27 @@ async function initSetupVbenForm() { }, }); } -function useVbenForm( - options: FormProps, +function useVbenForm< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + options: FormProps< + ComponentType, + ComponentPropsMap, + TFormValues, + TSubmitValues + >, ) { - return useForm(options); + return useForm( + options, + ); } export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; -export type VbenFormProps = FormProps< - ComponentType, - ComponentPropsMap, - TValues ->; +export type VbenFormProps< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +> = FormProps; diff --git a/apps/web-antdv-next/src/adapter/vxe-table.ts b/apps/web-antdv-next/src/adapter/vxe-table.ts index 0ddab3f6..5f492158 100644 --- a/apps/web-antdv-next/src/adapter/vxe-table.ts +++ b/apps/web-antdv-next/src/adapter/vxe-table.ts @@ -1,3 +1,4 @@ +import type { FormValues } from '@vben/common-ui'; import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; import type { ComponentPropsMap, ComponentType } from './component'; @@ -70,8 +71,23 @@ setupVbenVxeTable({ useVbenForm, }); -export const useVbenVxeGrid = >( - ...rest: Parameters> -) => useGrid(...rest); +export const useVbenVxeGrid = < + T extends Record, + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + ...rest: Parameters< + typeof useGrid< + T, + ComponentType, + ComponentPropsMap, + TFormValues, + TSubmitValues + > + > +) => + useGrid( + ...rest, + ); export type * from '@vben/plugins/vxe-table'; diff --git a/apps/web-ele/src/adapter/form.ts b/apps/web-ele/src/adapter/form.ts index 8c50f4a2..9edeb768 100644 --- a/apps/web-ele/src/adapter/form.ts +++ b/apps/web-ele/src/adapter/form.ts @@ -34,18 +34,27 @@ async function initSetupVbenForm() { }); } -function useVbenForm( - options: FormProps, +function useVbenForm< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + options: FormProps< + ComponentType, + ComponentPropsMap, + TFormValues, + TSubmitValues + >, ) { - return useForm(options); + return useForm( + options, + ); } export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; -export type VbenFormProps = FormProps< - ComponentType, - ComponentPropsMap, - TValues ->; +export type VbenFormProps< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +> = FormProps; diff --git a/apps/web-ele/src/adapter/vxe-table.ts b/apps/web-ele/src/adapter/vxe-table.ts index b044028c..1a9b268d 100644 --- a/apps/web-ele/src/adapter/vxe-table.ts +++ b/apps/web-ele/src/adapter/vxe-table.ts @@ -1,3 +1,4 @@ +import type { FormValues } from '@vben/common-ui'; import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; import type { ComponentPropsMap, ComponentType } from './component'; @@ -71,8 +72,23 @@ setupVbenVxeTable({ useVbenForm, }); -export const useVbenVxeGrid = >( - ...rest: Parameters> -) => useGrid(...rest); +export const useVbenVxeGrid = < + T extends Record, + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + ...rest: Parameters< + typeof useGrid< + T, + ComponentType, + ComponentPropsMap, + TFormValues, + TSubmitValues + > + > +) => + useGrid( + ...rest, + ); export type * from '@vben/plugins/vxe-table'; diff --git a/apps/web-naive/src/adapter/form.ts b/apps/web-naive/src/adapter/form.ts index d2b97322..16421682 100644 --- a/apps/web-naive/src/adapter/form.ts +++ b/apps/web-naive/src/adapter/form.ts @@ -38,18 +38,27 @@ async function initSetupVbenForm() { }); } -function useVbenForm( - options: FormProps, +function useVbenForm< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + options: FormProps< + ComponentType, + ComponentPropsMap, + TFormValues, + TSubmitValues + >, ) { - return useForm(options); + return useForm( + options, + ); } export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; -export type VbenFormProps = FormProps< - ComponentType, - ComponentPropsMap, - TValues ->; +export type VbenFormProps< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +> = FormProps; diff --git a/apps/web-naive/src/adapter/vxe-table.ts b/apps/web-naive/src/adapter/vxe-table.ts index 65fda685..538e6392 100644 --- a/apps/web-naive/src/adapter/vxe-table.ts +++ b/apps/web-naive/src/adapter/vxe-table.ts @@ -1,3 +1,4 @@ +import type { FormValues } from '@vben/common-ui'; import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; import type { ComponentPropsMap, ComponentType } from './component'; @@ -70,8 +71,23 @@ setupVbenVxeTable({ useVbenForm, }); -export const useVbenVxeGrid = >( - ...rest: Parameters> -) => useGrid(...rest); +export const useVbenVxeGrid = < + T extends Record, + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + ...rest: Parameters< + typeof useGrid< + T, + ComponentType, + ComponentPropsMap, + TFormValues, + TSubmitValues + > + > +) => + useGrid( + ...rest, + ); export type * from '@vben/plugins/vxe-table'; diff --git a/apps/web-tdesign/src/adapter/form.ts b/apps/web-tdesign/src/adapter/form.ts index 37002dff..34f12166 100644 --- a/apps/web-tdesign/src/adapter/form.ts +++ b/apps/web-tdesign/src/adapter/form.ts @@ -42,18 +42,27 @@ async function initSetupVbenForm() { }); } -function useVbenForm( - options: FormProps, +function useVbenForm< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + options: FormProps< + ComponentType, + ComponentPropsMap, + TFormValues, + TSubmitValues + >, ) { - return useForm(options); + return useForm( + options, + ); } export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; -export type VbenFormProps = FormProps< - ComponentType, - ComponentPropsMap, - TValues ->; +export type VbenFormProps< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +> = FormProps; diff --git a/apps/web-tdesign/src/adapter/vxe-table.ts b/apps/web-tdesign/src/adapter/vxe-table.ts index 6901c339..c621320b 100644 --- a/apps/web-tdesign/src/adapter/vxe-table.ts +++ b/apps/web-tdesign/src/adapter/vxe-table.ts @@ -1,3 +1,4 @@ +import type { FormValues } from '@vben/common-ui'; import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; import type { ComponentPropsMap, ComponentType } from './component'; @@ -70,8 +71,23 @@ setupVbenVxeTable({ useVbenForm, }); -export const useVbenVxeGrid = >( - ...rest: Parameters> -) => useGrid(...rest); +export const useVbenVxeGrid = < + T extends Record, + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + ...rest: Parameters< + typeof useGrid< + T, + ComponentType, + ComponentPropsMap, + TFormValues, + TSubmitValues + > + > +) => + useGrid( + ...rest, + ); export type * from '@vben/plugins/vxe-table'; diff --git a/docs/src/_env/adapter/form.ts b/docs/src/_env/adapter/form.ts index a72db5cc..ab390f6a 100644 --- a/docs/src/_env/adapter/form.ts +++ b/docs/src/_env/adapter/form.ts @@ -41,18 +41,30 @@ setupVbenForm({ }, }); -function useVbenForm( - options: FormProps, TValues>, +function useVbenForm< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +>( + options: FormProps< + ComponentType, + Record, + TFormValues, + TSubmitValues + >, ) { - return useForm>(options); + return useForm< + TFormValues, + ComponentType, + Record, + TSubmitValues + >(options); } export { useVbenForm, z }; export type VbenFormSchema = FormSchema, TValues>; -export type VbenFormProps = FormProps< - ComponentType, - Record, - TValues ->; +export type VbenFormProps< + TFormValues extends FormValues = FormValues, + TSubmitValues extends FormValues = TFormValues, +> = FormProps, TFormValues, TSubmitValues>; diff --git a/docs/src/components/common-ui/vben-form.md b/docs/src/components/common-ui/vben-form.md index c9bfa382..ccb2419b 100644 --- a/docs/src/components/common-ui/vben-form.md +++ b/docs/src/components/common-ui/vben-form.md @@ -244,13 +244,28 @@ export { initComponentAdapter }; -## 值格式化 +## 表单值编解码 -当组件的展示值与后端真正需要的 payload 不一致时,可以在 schema 上使用 `valueFormat`。它会在 `getValues()`、提交、以及依赖这些输出的方法中生效。 +当组件值与后端 payload 不一致时,使用表单级 `codec` 统一定义双向转换。`encode` 接收完整 `TFormValues` 并返回完整 `TSubmitValues`;`decode` 执行反向转换。多字段拆分、合并和删除都在一个纯函数边界完成,不依赖 schema 顺序或字符串路径写入。 -- `return xxx`:回写当前字段 -- `setValue('startTime', xxx)`:写入其他字段 -- `return undefined`:保持当前字段已被移除,适合把一个字段拆成多个字段 +`codec` 直接写在 `useVbenForm` 选项中即可。只需标注 `encode` 的表单值入参,`TSubmitValues` 会从返回对象自动推导,并传递给 `decode`、`getValues()` 和提交回调: + +```ts +const [Form, formApi] = useVbenForm({ + codec: { + decode(values) { + return { period: [values.startTime, values.endTime] }; + }, + encode(values: Readonly) { + return { + endTime: values.period[1], + startTime: values.period[0], + }; + }, + }, + schema, +}); +``` @@ -305,7 +320,7 @@ const [Form, formApi] = useVbenForm({ ### 类型传递与插槽 -通过 `useVbenForm` 定义一次表单值类型后,`getValues`、`setValues`、`setFieldValue`、`handleSubmit`、`handleValuesChange`、`formApi.form.values` 和 selector 都会沿用该类型,可直接作为 API 请求参数: +使用 `useVbenForm` 分别声明组件表单值和提交值。schema、slots、`setValues`、`formApi.form.values` 使用 `TFormValues`;`getValues`、submit 和 `handleSubmit` 第一参数使用 `TSubmitValues`。两种结构相同时只传一个泛型即可。 ```vue