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
This commit is contained in:
dream-weave
2026-07-23 15:09:31 +08:00
committed by GitHub
parent f43de252b5
commit 98dea92225
43 changed files with 1678 additions and 413 deletions

View File

@@ -41,18 +41,27 @@ async function initSetupVbenForm() {
});
}
function useVbenForm<TValues extends FormValues = FormValues>(
options: FormProps<ComponentType, ComponentPropsMap, TValues>,
function useVbenForm<
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
>(
options: FormProps<
ComponentType,
ComponentPropsMap,
TFormValues,
TSubmitValues
>,
) {
return useForm<TValues, ComponentType, ComponentPropsMap>(options);
return useForm<TFormValues, ComponentType, ComponentPropsMap, TSubmitValues>(
options,
);
}
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema<TValues extends FormValues = FormValues> =
FormSchema<ComponentType, ComponentPropsMap, TValues>;
export type VbenFormProps<TValues extends FormValues = FormValues> = FormProps<
ComponentType,
ComponentPropsMap,
TValues
>;
export type VbenFormProps<
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
> = FormProps<ComponentType, ComponentPropsMap, TFormValues, TSubmitValues>;

View File

@@ -1,4 +1,4 @@
import type { TableActionProps } from '@vben/common-ui';
import type { FormValues, TableActionProps } from '@vben/common-ui';
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { Recordable } from '@vben/types';
@@ -287,9 +287,24 @@ setupVbenVxeTable({
},
});
export const useVbenVxeGrid = <T extends Record<string, any>>(
...rest: Parameters<typeof useGrid<T, ComponentType, ComponentPropsMap>>
) => useGrid<T, ComponentType, ComponentPropsMap>(...rest);
export const useVbenVxeGrid = <
T extends Record<string, any>,
TFormValues extends FormValues = FormValues,
TSubmitValues extends FormValues = TFormValues,
>(
...rest: Parameters<
typeof useGrid<
T,
ComponentType,
ComponentPropsMap,
TFormValues,
TSubmitValues
>
>
) =>
useGrid<T, ComponentType, ComponentPropsMap, TFormValues, TSubmitValues>(
...rest,
);
/**
* 表格操作按钮组件

View File

@@ -91,7 +91,6 @@ contacts[0].name;
- `help`
- `suffix`
- `renderComponentContent`
- `valueFormat`
- `formFieldProps`
- `disabled`
- `hide`
@@ -188,29 +187,29 @@ dependencies: {
triggerFields: ['$row.role'];
```
## valueFormat 用法
## 提交值转换
children 里的 `valueFormat` 也会按行执行:
数组字段的提交转换使用表单级 `codec`,一次处理完整表单值:
```ts
{
component: 'Input',
fieldName: 'phone',
label: '电话',
valueFormat: (value, setValue) => {
const nextValue = value?.trim();
if (!nextValue) {
return;
}
setValue('phone', nextValue);
},
function encodeArrayFormValues(values: Readonly<ArrayFormValues>) {
return {
...values,
contacts: values.contacts.map((contact) => ({
...contact,
name: contact.name.trim(),
phone: contact.phone?.trim() || undefined,
})),
};
}
```
在 `contacts[0]` 中,`setValue('phone', nextValue)` 会自动写到:
```ts
contacts[0].phone;
const [Form] = useVbenForm({
codec: {
decode: decodeArrayFormValues,
encode: encodeArrayFormValues,
},
schema,
});
```
如果要写根字段,用 `$root.`:
@@ -284,7 +283,7 @@ flowchart TD
H --> I["createArrayChildSchema"]
I --> J["child.fieldName 转为 contacts[index].xxx"]
I --> K["scope dependencies triggerFields"]
I --> L["包装 componentProps/help/render/valueFormat ctx"]
I --> L["包装 componentProps/help/render ctx"]
J --> M["继续复用 FormField 渲染 child"]
```
@@ -294,7 +293,7 @@ flowchart TD
- 具体展示仍复用内部 `VbenFormFieldArray`。
- child 最终仍然走 `FormField`,所以现有 FormSchema 能力不会丢。
- `dependencies` 不改核心调用链,而是在 `createArrayChildSchema` 里做路径和 ctx 适配。
- `valueFormat` 和 `updateSchema` 在 `FormApi` 里递归处理 children。
- `updateSchema` 在 `FormApi` 里递归处理 children;提交转换由表单级 `codec` 统一完成。
## 小屏幕展示

View File

@@ -9,7 +9,48 @@ import { Button, Card, message, Space } from 'antdv-next';
import { useVbenForm, z } from '#/adapter/form';
const submitValues = ref<Record<string, any>>({});
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),
);
@@ -23,7 +64,7 @@ const outputClass = [
'text-xs',
];
const schema: VbenFormSchema[] = [
const schema: VbenFormSchema<ArrayFormValues>[] = [
{
component: 'Input',
componentProps: {
@@ -38,7 +79,7 @@ const schema: VbenFormSchema[] = [
component: 'Textarea',
dependencies: {
componentProps: (values) => {
const planName = values.planName as string | undefined;
const planName = values.planName;
return {
disabled: !planName,
placeholder: planName ? `${planName} 的补充说明` : '请先填写方案名称',
@@ -81,7 +122,6 @@ const schema: VbenFormSchema[] = [
fieldName: 'name',
label: '姓名',
rules: z.string().min(1, '请输入姓名'),
valueFormat: (value) => value?.trim(),
},
{
component: 'Select',
@@ -110,13 +150,6 @@ const schema: VbenFormSchema[] = [
fieldName: 'phone',
label: '电话',
rules: z.string().optional(),
valueFormat: (value, setValue) => {
const nextValue = value?.trim();
if (!nextValue) {
return;
}
setValue('phone', nextValue);
},
},
{
component: 'Switch',
@@ -146,6 +179,10 @@ const schema: VbenFormSchema[] = [
];
const [Form, formApi] = useVbenForm({
codec: {
decode: decodeArrayFormValues,
encode: encodeArrayFormValues,
},
commonConfig: {
labelWidth: 90,
},

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { UploadFile } from 'antdv-next';
import type { Dayjs } from 'dayjs';
import { h, ref, toRaw } from 'vue';
@@ -18,6 +19,36 @@ import DocButton from '../doc-button.vue';
const keyword = ref('');
const fetching = ref(false);
interface BasicFormValues extends Record<string, any> {
cropImage?: UploadFile[];
files?: UploadFile[];
rangePicker?: [Dayjs, Dayjs];
}
function encodeBasicFormValues(values: Readonly<BasicFormValues>) {
const { rangePicker, ...formValues } = values;
return {
...formValues,
endTime: rangePicker?.[1]?.format('YYYY-MM-DD'),
startTime: rangePicker?.[0]?.format('YYYY-MM-DD'),
};
}
type BasicSubmitValues = ReturnType<typeof encodeBasicFormValues>;
function decodeBasicFormValues(
values: Readonly<BasicSubmitValues>,
): BasicFormValues {
const { endTime, startTime, ...formValues } = values;
return {
...formValues,
...(startTime && endTime
? { rangePicker: [dayjs(startTime), dayjs(endTime)] }
: {}),
};
}
// 模拟远程获取数据
function fetchRemoteOptions({ keyword = '选项' }: Record<string, any>) {
fetching.value = true;
@@ -34,6 +65,10 @@ function fetchRemoteOptions({ keyword = '选项' }: Record<string, any>) {
}
const [BaseForm, baseFormApi] = useVbenForm({
codec: {
decode: decodeBasicFormValues,
encode: encodeBasicFormValues,
},
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 在label后显示一个冒号
@@ -43,7 +78,6 @@ const [BaseForm, baseFormApi] = useVbenForm({
class: 'w-full',
},
},
fieldMappingTime: [['rangePicker', ['startTime', 'endTime'], 'YYYY-MM-DD']],
// 提交函数
handleSubmit: onSubmit,
handleValuesChange(_values, fieldsChanged) {
@@ -425,8 +459,8 @@ const [BaseForm, baseFormApi] = useVbenForm({
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
function onSubmit(values: Record<string, any>) {
const files = toRaw(values.files) as UploadFile[];
function onSubmit(values: BasicSubmitValues) {
const files = (toRaw(values.files) ?? []) as UploadFile[];
const cropImage = (toRaw(values.cropImage) ?? []) as UploadFile[];
const doneFiles = files.filter((file) => file.status === 'done');
const failedFiles = files.filter((file) => file.status !== 'done');

View File

@@ -9,7 +9,40 @@ import { useVbenForm, z } from '#/adapter/form';
import TwoFields from './modules/two-fields.vue';
interface CustomFormValues extends Record<string, unknown> {
field?: string;
field1?: string;
field2?: string;
field3?: string;
field4?: [string | undefined, string];
}
function encodeCustomFormValues(values: Readonly<CustomFormValues>) {
const { field4, ...formValues } = values;
return {
...formValues,
phoneNumber: field4?.[1],
phoneType: field4?.[0],
};
}
type CustomSubmitValues = ReturnType<typeof encodeCustomFormValues>;
function decodeCustomFormValues(
values: Readonly<CustomSubmitValues>,
): CustomFormValues {
const { phoneNumber, phoneType, ...formValues } = values;
return {
...formValues,
field4: [phoneType, phoneNumber ?? ''],
};
}
const [Form] = useVbenForm({
codec: {
decode: decodeCustomFormValues,
encode: encodeCustomFormValues,
},
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
@@ -18,7 +51,6 @@ const [Form] = useVbenForm({
},
labelClass: 'w-2/6',
},
fieldMappingTime: [['field4', ['phoneType', 'phoneNumber'], null]],
// 提交函数
handleSubmit: onSubmit,
// 垂直布局,label和input在不同行,值为vertical
@@ -80,7 +112,7 @@ const [Form] = useVbenForm({
wrapperClass: 'grid-cols-1 md:grid-cols-2',
});
function onSubmit(values: Record<string, any>) {
function onSubmit(values: CustomSubmitValues) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
});

View File

@@ -1,5 +1,7 @@
<script lang="ts" setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import type { Dayjs } from 'dayjs';
import { computed, nextTick, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
@@ -10,35 +12,65 @@ import { useVbenForm } from '#/adapter/form';
import DocButton from '../doc-button.vue';
const transformedValues = ref<Record<string, any>>({});
const liveValues = ref<Record<string, any>>({});
interface ValueFormatFormValues {
deadline?: Dayjs;
keyword?: string;
reportRange?: [Dayjs, Dayjs];
}
function encodeValueFormatValues(values: Readonly<ValueFormatFormValues>) {
return {
deadline: values.deadline?.valueOf(),
endTime: values.reportRange?.[1].valueOf(),
keyword: values.keyword,
startTime: values.reportRange?.[0].valueOf(),
};
}
type ValueFormatSubmitValues = ReturnType<typeof encodeValueFormatValues>;
function decodeValueFormatValues(
values: Readonly<ValueFormatSubmitValues>,
): ValueFormatFormValues {
let reportRange: [Dayjs, Dayjs] | undefined;
if (values.startTime !== undefined && values.endTime !== undefined) {
reportRange = [dayjs(values.startTime), dayjs(values.endTime)];
}
return {
deadline:
values.deadline === undefined ? undefined : dayjs(values.deadline),
keyword: values.keyword,
reportRange,
};
}
const transformedValues = ref<Partial<ValueFormatSubmitValues>>({});
const liveValues = ref<Partial<ValueFormatFormValues>>({});
const [Form, formApi] = useVbenForm({
codec: {
decode: decodeValueFormatValues,
encode: encodeValueFormatValues,
},
commonConfig: {
componentProps: {
class: 'w-full',
},
},
handleSubmit,
handleValuesChange,
schema: [
{
component: 'RangePicker',
fieldName: 'reportRange',
help: '通过 setValue 拆分为 startTime / endTime,并移除原字段',
help: '由表单 codec 拆分为 startTime / endTime',
label: '统计时间范围',
valueFormat(value, setValue) {
setValue('startTime', value?.[0]?.valueOf());
setValue('endTime', value?.[1]?.valueOf());
},
},
{
component: 'DatePicker',
fieldName: 'deadline',
help: '直接 return 时间戳,保留原字段名',
help: '由表单 codec 编码为时间戳',
label: '截止时间',
valueFormat(value) {
return value?.valueOf();
},
},
{
component: 'Input',
@@ -58,7 +90,7 @@ const transformedValuesPreview = computed(() => {
return formatJsonPreview(transformedValues.value);
});
function formatJsonPreview(value: Record<string, any>) {
function formatJsonPreview(value: unknown) {
return JSON.stringify(
value,
(_key, currentValue) => {
@@ -83,49 +115,50 @@ function handleSetExampleValue() {
});
}
function handleSubmit(values: Record<string, any>) {
function handleSubmit(values: ValueFormatSubmitValues) {
transformedValues.value = values;
message.success({
content: `getValues output: ${JSON.stringify(values)}`,
});
}
async function syncPreviewValues(values?: Record<string, any>) {
liveValues.value = values ?? formApi.form?.values ?? {};
function handleValuesChange(
values: Readonly<ValueFormatFormValues>,
_fieldsChanged: string[],
getFormattedValues: () => ValueFormatSubmitValues,
) {
liveValues.value = { ...values };
transformedValues.value = getFormattedValues();
}
async function syncPreviewValues(values?: Readonly<ValueFormatFormValues>) {
liveValues.value = { ...(values ?? formApi.form?.values) };
transformedValues.value = await formApi.getValues();
}
onMounted(async () => {
await nextTick();
watch(
() => formApi.form?.values,
async (values) => {
await syncPreviewValues(values);
},
{
deep: true,
immediate: true,
},
);
await syncPreviewValues();
});
</script>
<template>
<Page
content-class="flex flex-col gap-4"
description="演示 schema.valueFormat 如何把组件值转换为提交/查询所需的 payload。"
title="表单 valueFormat"
description="演示表单级 codec 如何双向转换组件值和提交 payload。"
title="表单 Codec"
>
<template #description>
<div class="text-muted-foreground space-y-2">
<p>
<code>form.values</code> 保持组件原始值,<code>getValues()</code> /
提交时会按 <code>schema.valueFormat</code> 输出转换后的 payload。
提交时会按 <code>codec.encode</code> 输出 payload,回填时通过
<code>codec.decode</code> 恢复组件值。
</p>
<div class="flex flex-wrap gap-2">
<Tag color="processing">return 值:回写当前字段</Tag>
<Tag color="success">setValue:拆分写入其他字段</Tag>
<Tag color="warning">return undefined:保持原字段删除</Tag>
<Tag color="processing">encode:生成完整提交值</Tag>
<Tag color="success">decode:恢复完整表单值</Tag>
<Tag color="warning">多字段转换原子执行</Tag>
</div>
</div>
</template>
@@ -133,7 +166,7 @@ onMounted(async () => {
<DocButton path="/components/common-ui/vben-form" />
</template>
<Card title="valueFormat 示例">
<Card title="Codec 示例">
<template #extra>
<Space wrap>
<Button @click="handleSetExampleValue">填充示例数据</Button>
@@ -151,7 +184,7 @@ onMounted(async () => {
liveValuesPreview
}}</pre>
</Card>
<Card title="getValues / submit 输出(valueFormat 后)">
<Card title="getValues / submit 输出(codec.encode 后)">
<pre class="bg-muted overflow-auto rounded-md p-4 text-sm">{{
transformedValuesPreview
}}</pre>

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import type { Dayjs } from 'dayjs';
import type { VbenFormProps } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
@@ -19,10 +21,42 @@ interface RowType {
releaseDate: string;
}
const formOptions: VbenFormProps = {
interface SearchFormValues extends Record<string, unknown> {
category?: string;
color?: string;
date?: [Dayjs, Dayjs];
price?: string;
productName?: string;
}
function encodeSearchFormValues(values: Readonly<SearchFormValues>) {
const { date, ...formValues } = values;
return {
...formValues,
end: date?.[1]?.format('YYYY-MM-DD'),
start: date?.[0]?.format('YYYY-MM-DD'),
};
}
type SearchSubmitValues = ReturnType<typeof encodeSearchFormValues>;
function decodeSearchFormValues(
values: Readonly<SearchSubmitValues>,
): SearchFormValues {
const { end, start, ...formValues } = values;
return {
...formValues,
...(start && end ? { date: [dayjs(start), dayjs(end)] } : {}),
};
}
const formOptions: VbenFormProps<SearchFormValues, SearchSubmitValues> = {
codec: {
decode: decodeSearchFormValues,
encode: encodeSearchFormValues,
},
// 默认展开
collapsed: false,
fieldMappingTime: [['date', ['start', 'end']]],
schema: [
{
component: 'Input',
@@ -94,7 +128,7 @@ const gridOptions: VxeTableGridOptions<RowType> = {
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
query: async ({ page }, formValues: SearchSubmitValues) => {
message.success(`Query params: ${JSON.stringify(formValues)}`);
return await getExampleTableApi({
page: page.currentPage,

View File

@@ -31,7 +31,42 @@ const emit = defineEmits<{
}>();
const formData = ref<SystemMenuApi.SystemMenu>();
const titleSuffix = ref<string>();
const schema: VbenFormSchema[] = [
type MenuSubmitValues = Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>;
type MenuFormValues = MenuSubmitValues & { linkSrc?: string };
function encodeMenuFormValues(
values: Readonly<MenuFormValues>,
): MenuSubmitValues {
const { linkSrc, ...formValues } = values;
if (values.type === 'link') {
return {
...formValues,
meta: { ...values.meta, link: linkSrc },
};
}
if (values.type === 'embedded') {
return {
...formValues,
meta: { ...values.meta, iframeSrc: linkSrc },
};
}
return formValues;
}
function decodeMenuFormValues(
values: Readonly<MenuSubmitValues>,
): MenuFormValues {
let linkSrc: string | undefined;
if (values.type === 'link') {
linkSrc = values.meta?.link;
} else if (values.type === 'embedded') {
linkSrc = values.meta?.iframeSrc;
}
return { ...values, linkSrc };
}
const schema: VbenFormSchema<MenuFormValues>[] = [
{
component: 'RadioGroup',
componentProps: {
@@ -428,6 +463,10 @@ const breakpoints = useBreakpoints(breakpointsTailwind);
const isHorizontal = computed(() => breakpoints.greaterOrEqual('md').value);
const [Form, formApi] = useVbenForm({
codec: {
decode: decodeMenuFormValues,
encode: encodeMenuFormValues,
},
commonConfig: {
colon: true,
formItemClass: 'col-span-2 md:col-span-1',
@@ -438,17 +477,12 @@ const [Form, formApi] = useVbenForm({
});
const [Drawer, drawerApi] = useVbenDrawer({
onConfirm: onSubmit,
onOpenChange(isOpen) {
async onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemMenuApi.SystemMenu>();
if (data?.type === 'link') {
data.linkSrc = data.meta?.link;
} else if (data?.type === 'embedded') {
data.linkSrc = data.meta?.iframeSrc;
}
if (data) {
formData.value = data;
formApi.setValues(formData.value);
await formApi.setSubmitValues(data);
titleSuffix.value = formData.value.meta?.title
? $t(formData.value.meta.title)
: '';
@@ -464,16 +498,7 @@ async function onSubmit() {
const { valid } = await formApi.validate();
if (valid) {
drawerApi.lock();
const data =
await formApi.getValues<
Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>
>();
if (data.type === 'link') {
data.meta = { ...data.meta, link: data.linkSrc };
} else if (data.type === 'embedded') {
data.meta = { ...data.meta, iframeSrc: data.linkSrc };
}
delete data.linkSrc;
const data = await formApi.getValues();
try {
await (formData.value?.id
? updateMenu(formData.value.id, data)

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import type { Dayjs } from 'dayjs';
import type { Recordable } from '@vben/types';
import type {
@@ -11,6 +13,7 @@ import { Page, useVbenDrawer } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message, Modal } from 'antdv-next';
import dayjs from 'dayjs';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteRole, getRoleList, updateRole } from '#/api';
@@ -19,6 +22,33 @@ import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
interface RoleSearchFormValues extends Record<string, unknown> {
createTime?: [Dayjs, Dayjs];
}
function encodeRoleSearchValues(values: Readonly<RoleSearchFormValues>) {
const { createTime, ...formValues } = values;
return {
...formValues,
endTime: createTime?.[1]?.format('YYYY-MM-DD'),
startTime: createTime?.[0]?.format('YYYY-MM-DD'),
};
}
type RoleSearchSubmitValues = ReturnType<typeof encodeRoleSearchValues>;
function decodeRoleSearchValues(
values: Readonly<RoleSearchSubmitValues>,
): RoleSearchFormValues {
const { endTime, startTime, ...formValues } = values;
return {
...formValues,
...(startTime && endTime
? { createTime: [dayjs(startTime), dayjs(endTime)] }
: {}),
};
}
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
@@ -26,7 +56,10 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
codec: {
decode: decodeRoleSearchValues,
encode: encodeRoleSearchValues,
},
schema: useGridFormSchema(),
submitOnChange: true,
},
@@ -36,7 +69,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
query: async ({ page }, formValues: RoleSearchSubmitValues) => {
return await getRoleList({
page: page.currentPage,
pageSize: page.pageSize,

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import type { Dayjs } from 'dayjs';
import type { Recordable } from '@vben/types';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
@@ -10,6 +12,7 @@ import { Page, Tree, useVbenDrawer } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, Card, InputSearch, message, Modal } from 'antdv-next';
import dayjs from 'dayjs';
import { useVbenVxeGrid, VbenTableAction } from '#/adapter/vxe-table';
import { deleteUser, getDeptList, getUserList, updateUser } from '#/api';
@@ -19,6 +22,33 @@ import { useColumns, useGridFormSchema } from './data';
import Detail from './modules/detail.vue';
import Form from './modules/form.vue';
interface UserSearchFormValues extends Record<string, unknown> {
createTime?: [Dayjs, Dayjs];
}
function encodeUserSearchValues(values: Readonly<UserSearchFormValues>) {
const { createTime, ...formValues } = values;
return {
...formValues,
endTime: createTime?.[1]?.format('YYYY-MM-DD'),
startTime: createTime?.[0]?.format('YYYY-MM-DD'),
};
}
type UserSearchSubmitValues = ReturnType<typeof encodeUserSearchValues>;
function decodeUserSearchValues(
values: Readonly<UserSearchSubmitValues>,
): UserSearchFormValues {
const { endTime, startTime, ...formValues } = values;
return {
...formValues,
...(startTime && endTime
? { createTime: [dayjs(startTime), dayjs(endTime)] }
: {}),
};
}
const deptList = ref<SystemDeptApi.SystemDept[]>([]);
const inputSearchValue = ref('');
const selectedDeptId = ref<string>('');
@@ -35,7 +65,10 @@ const [DetailDrawer, detailDrawerApi] = useVbenDrawer({
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
codec: {
decode: decodeUserSearchValues,
encode: encodeUserSearchValues,
},
schema: useGridFormSchema(),
submitOnChange: true,
},
@@ -45,7 +78,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
query: async ({ page }, formValues: UserSearchSubmitValues) => {
return await getUserList({
page: page.currentPage,
pageSize: page.pageSize,