Files
lgp-admin-plus/playground/src/views/demos/form-array/index.vue
dream-weave 98dea92225 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
2026-07-23 15:09:31 +08:00

235 lines
5.7 KiB
Vue

<script setup lang="ts">
import type { VbenFormSchema } from '#/adapter/form';
import { computed, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, message, Space } from 'antdv-next';
import { useVbenForm, z } from '#/adapter/form';
interface ContactFormValues {
enabled: boolean;
name: string;
phone?: string;
role: 'member' | 'owner' | 'viewer';
}
interface ArrayFormValues extends Record<string, unknown> {
contacts: ContactFormValues[];
description?: string;
planName: string;
}
function encodeArrayFormValues(values: Readonly<ArrayFormValues>) {
return {
...values,
contacts: values.contacts.map(({ phone, ...contact }) => {
const trimmedPhone = phone?.trim();
return {
...contact,
name: contact.name.trim(),
...(trimmedPhone ? { phone: trimmedPhone } : {}),
};
}),
};
}
type ArraySubmitValues = ReturnType<typeof encodeArrayFormValues>;
function decodeArrayFormValues(
values: Readonly<ArraySubmitValues>,
): ArrayFormValues {
return {
...values,
contacts: values.contacts.map((contact) => ({
...contact,
phone: contact.phone ?? '',
})),
};
}
const submitValues = ref<Partial<ArraySubmitValues>>({});
const formattedSubmitValues = computed(() =>
JSON.stringify(submitValues.value, null, 2),
);
const outputClass = [
'bg-muted',
'text-muted-foreground',
'max-h-[420px]',
'overflow-auto',
'rounded-md',
'p-3',
'text-xs',
];
const schema: VbenFormSchema<ArrayFormValues>[] = [
{
component: 'Input',
componentProps: {
placeholder: '请输入方案名称',
},
defaultValue: '值班联络人配置',
fieldName: 'planName',
label: '方案名称',
rules: z.string().min(1, '请输入方案名称'),
},
{
component: 'Textarea',
dependencies: {
componentProps: (values) => {
const planName = values.planName;
return {
disabled: !planName,
placeholder: planName ? `${planName} 的补充说明` : '请先填写方案名称',
rows: 2,
};
},
required: (values) => {
return String(values.planName ?? '').includes('值班');
},
rules: (values) => {
return String(values.planName ?? '').includes('值班')
? z.string().min(2, '请输入至少 2 个字')
: z.string().optional();
},
triggerFields: ['planName'],
},
fieldName: 'description',
formItemClass: 'col-span-1 md:col-span-2',
label: '方案说明',
},
{
arrayProps: {
addButtonText: '添加联系人',
createRow: () => ({
enabled: true,
name: '',
phone: '',
role: 'member',
}),
max: 5,
min: 1,
},
children: [
{
component: 'Input',
componentProps: (ctx) => ({
placeholder: `第 ${(ctx.rowIndex ?? 0) + 1} 行姓名`,
}),
defaultValue: '',
fieldName: 'name',
label: '姓名',
rules: z.string().min(1, '请输入姓名'),
},
{
component: 'Select',
componentProps: {
options: [
{ label: '负责人', value: 'owner' },
{ label: '成员', value: 'member' },
{ label: '观察员', value: 'viewer' },
],
},
defaultValue: 'member',
fieldName: 'role',
label: '角色',
rules: 'selectRequired',
},
{
component: 'Input',
dependencies: {
componentProps: (_values, _form, _api, ctx) => ({
disabled: ctx?.row?.role === 'viewer',
placeholder:
ctx?.row?.role === 'viewer' ? '观察员无需电话' : '请输入电话',
}),
triggerFields: ['role'],
},
fieldName: 'phone',
label: '电话',
rules: z.string().optional(),
},
{
component: 'Switch',
componentProps: {
checkedChildren: '启用',
unCheckedChildren: '停用',
},
defaultValue: true,
fieldName: 'enabled',
label: '状态',
},
],
defaultValue: [
{
enabled: true,
name: '张三',
phone: ' 10086 ',
role: 'owner',
},
],
fieldName: 'contacts',
formItemClass: 'col-span-1 md:col-span-2',
label: '联系人',
rules: z.array(z.any()).min(1, '请至少添加一个联系人'),
type: 'array',
},
];
const [Form, formApi] = useVbenForm({
codec: {
decode: decodeArrayFormValues,
encode: encodeArrayFormValues,
},
commonConfig: {
labelWidth: 90,
},
handleSubmit: (values) => {
submitValues.value = values;
message.success('已通过校验');
},
schema,
showDefaultActions: false,
wrapperClass: 'grid-cols-1 gap-x-4 md:grid-cols-2',
});
async function handleSubmit() {
await formApi.validateAndSubmit();
}
async function handleGetValues() {
submitValues.value = await formApi.getValues();
}
function handlePatchChildRule() {
formApi.updateSchema([
{
fieldName: 'contacts.phone',
rules: z.string().min(5, '电话至少 5 位'),
},
]);
message.success('已动态更新子字段规则');
}
</script>
<template>
<Page title="Form Array Demo">
<div class="grid gap-4 xl:grid-cols-[minmax(0,1fr)_360px]">
<Card title="数组字段">
<Form />
<Space class="mt-4 flex flex-wrap">
<Button type="primary" @click="handleSubmit">提交</Button>
<Button @click="handleGetValues">获取值</Button>
<Button @click="handlePatchChildRule">更新电话规则</Button>
</Space>
</Card>
<Card title="输出">
<pre :class="outputClass" v-text="formattedSubmitValues"></pre>
</Card>
</div>
</Page>
</template>