feat(form): support useFieldTextInError option

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
roymondchen 2026-06-26 16:38:40 +08:00
parent 5e661d0958
commit fd359e493f
10 changed files with 113 additions and 1 deletions

View File

@ -150,6 +150,14 @@
- **类型:** `boolean`
## useFieldTextInError
- **详情:** 透传给内部 `Form`,控制表单校验失败时错误提示前缀是否使用字段的 `text` 文案。`false` 时直接使用字段 `name`
- **默认值:** `true`
- **类型:** `boolean`
## closeOnClickModal
- **详情:** 是否可以通过点击 modal 关闭 Dialog

View File

@ -40,6 +40,8 @@
- **详情:** 通过 `name` 从表单 `config` 中查找对应表单项的 `text`
- **相关:** 表单校验失败时是否使用该方法生成错误提示前缀,由 [`useFieldTextInError`](./form-props.md#usefieldtextinerror) prop 控制(默认 `true`
## values
- **类型:** `Ref<FormValue>`

View File

@ -206,6 +206,19 @@
- **类型:** `boolean`
## useFieldTextInError
- **详情:**
表单校验失败时,错误提示前缀是否使用字段的 `text` 文案(通过 `getTextByName``config` 中查找)。
- `true`(默认):错误提示形如 `字段文案 -> 错误信息`,找不到 `text` 时回退为字段 `name`
- `false`:跳过查找,直接使用字段 `name` 作为错误提示前缀(形如 `字段name -> 错误信息`)。
- **默认值:** `true`
- **类型:** `boolean`
## extendState
- **详情:** 扩展 formState 的钩子函数,返回的对象会被合并到 formState 上

View File

@ -37,6 +37,7 @@ function submitForm(options: SubmitFormOptions): Promise<any>;
| `keyProp` | `string` | `'__key'` | 配置项的唯一 key |
| `popperClass` | `string` | — | 弹层 className |
| `preventSubmitDefault` | `boolean` | — | 是否阻止表单原生 submit |
| `useFieldTextInError` | `boolean` | `true` | 校验失败时错误提示前缀是否使用字段的 `text` 文案;`false` 时直接使用字段 `name` |
| `extendState` | `(state: FormState) => Record<string, any> \| Promise<Record<string, any>>` | — | 扩展 `formState` |
| `native` | `boolean` | `false` | 透传给 `Form.submitForm``true` 时返回内部响应式 `values`,否则返回 `cloneDeep(toRaw(values))` |
| `returnChangeRecords` | `boolean` | `false` | `true` 时 resolve 结果为 `{ values, changeRecords }`,携带表单变更记录;否则仅 resolve `values` |

View File

@ -79,6 +79,13 @@ const props = withDefaults(
keyProp?: string;
popperClass?: string;
preventSubmitDefault?: boolean;
/**
* 表单校验失败时错误提示前缀是否使用字段的 text 文案通过 `getTextByName` config 中查找
*
* - `true`默认错误提示形如 `字段文案 -> 错误信息`找不到 text 时回退为字段 name
* - `false`跳过查找直接使用字段 name 作为错误提示前缀形如 `字段name -> 错误信息`
*/
useFieldTextInError?: boolean;
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* 自定义"是否展示对比内容"的判断函数仅在 `isCompare === true` 时生效
@ -121,6 +128,7 @@ const props = withDefaults(
inline: false,
labelPosition: 'right',
keyProp: '__key',
useFieldTextInError: true,
},
);
@ -384,7 +392,7 @@ defineExpose({
Object.entries(invalidFields).forEach(([prop, ValidateError]) => {
(ValidateError as ValidateError[]).forEach(({ field, message }) => {
const name = field || prop;
const text = getTextByName(name, props.config) || name;
const text = (props.useFieldTextInError ? getTextByName(name, props.config) : undefined) || name;
error.push(`${text} -> ${message}`);
});

View File

@ -13,6 +13,7 @@
:label-position="labelPosition"
:inline="inline"
:prevent-submit-default="preventSubmitDefault"
:use-field-text-in-error="useFieldTextInError"
:extend-state="extendState"
@change="changeHandler"
></Form>
@ -61,12 +62,15 @@ const props = withDefaults(
inline?: boolean;
labelPosition?: string;
preventSubmitDefault?: boolean;
/** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
useFieldTextInError?: boolean;
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
}>(),
{
config: () => [],
values: () => ({}),
confirmText: '确定',
useFieldTextInError: true,
},
);

View File

@ -31,6 +31,7 @@
:label-position="labelPosition"
:inline="inline"
:prevent-submit-default="preventSubmitDefault"
:use-field-text-in-error="useFieldTextInError"
:extend-state="extendState"
@change="changeHandler"
></Form>
@ -96,6 +97,8 @@ const props = withDefaults(
destroyOnClose?: boolean;
showClose?: boolean;
showCancel?: boolean;
/** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
useFieldTextInError?: boolean;
/** 透传给内部 `MForm`,用于扩展 `formState`(如注入 `$message` / `$store` 等) */
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
}>(),
@ -108,6 +111,7 @@ const props = withDefaults(
destroyOnClose: false,
showClose: true,
showCancel: true,
useFieldTextInError: true,
},
);

View File

@ -28,6 +28,7 @@
:label-position="labelPosition"
:inline="inline"
:prevent-submit-default="preventSubmitDefault"
:use-field-text-in-error="useFieldTextInError"
:extend-state="extendState"
@change="changeHandler"
></Form>
@ -82,6 +83,8 @@ withDefaults(
inline?: boolean;
labelPosition?: string;
preventSubmitDefault?: boolean;
/** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
useFieldTextInError?: boolean;
/** 关闭前的回调,会暂停 Drawer 的关闭; done 是个 function type 接受一个 boolean 参数, 执行 done 使用 true 参数或不提供参数将会终止关闭 */
beforeClose?: (_done: (_cancel?: boolean) => void) => void;
/** 透传给内部 `MForm`,用于扩展 `formState`(如注入 `$message` / `$store` 等) */
@ -92,6 +95,7 @@ withDefaults(
config: () => [],
values: () => ({}),
confirmText: '确定',
useFieldTextInError: true,
},
);

View File

@ -45,6 +45,11 @@ export interface SubmitFormOptions {
keyProp?: string;
popperClass?: string;
preventSubmitDefault?: boolean;
/**
* 使 text `getTextByName` config
* `true` `false` 使 name
*/
useFieldTextInError?: boolean;
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/** 透传给 Form.submitForm 的参数:是否直接返回原始响应式 values */
native?: boolean;

View File

@ -340,6 +340,69 @@ describe('Form.vue —— submitForm 实例方法', () => {
});
});
describe('Form.vue —— useFieldTextInError', () => {
const mountAndMockValidate = async (
props: Record<string, any>,
invalidFields: Record<string, { field: string; message: string }[]>,
) => {
const wrapper = mountForm(props);
await nextTick();
const tmForm = wrapper.findComponent({ name: 'TMForm' });
const { exposed } = (tmForm.vm as any).$;
exposed.validate = vi.fn().mockRejectedValue(invalidFields);
let caught: Error | null = null;
try {
await wrapper.vm.submitForm();
} catch (e: any) {
caught = e;
}
return { wrapper, caught };
};
test('默认useFieldTextInError 未传)时错误信息使用 config 中的 text', async () => {
const { caught } = await mountAndMockValidate(
{
config: [{ text: '名称', type: 'text', name: 'name' }],
initValues: { name: '' },
},
{ name: [{ field: 'name', message: '必填' }] },
);
expect(caught!.message).toContain('名称');
expect(caught!.message).not.toContain('name -> ');
});
test('useFieldTextInError=true 时错误信息使用 config 中的 text', async () => {
const { caught } = await mountAndMockValidate(
{
config: [{ text: '名称', type: 'text', name: 'name' }],
initValues: { name: '' },
useFieldTextInError: true,
},
{ name: [{ field: 'name', message: '必填' }] },
);
expect(caught!.message).toContain('名称 -> 必填');
});
test('useFieldTextInError=false 时跳过查找,直接使用字段 name', async () => {
const { caught } = await mountAndMockValidate(
{
config: [{ text: '名称', type: 'text', name: 'name' }],
initValues: { name: '' },
useFieldTextInError: false,
},
{ name: [{ field: 'name', message: '必填' }] },
);
expect(caught!.message).toContain('name -> 必填');
expect(caught!.message).not.toContain('名称');
});
});
describe('Form.vue —— getTextByName', () => {
let wrapper: ReturnType<typeof mountForm>;