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:
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
/**
|
||||
* 表格操作按钮组件
|
||||
|
||||
@@ -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` 统一完成。
|
||||
|
||||
## 小屏幕展示
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)}`,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user