Merge branch 'main' into profile
This commit is contained in:
commit
d811af37dd
@ -335,6 +335,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
|||||||
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||||
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||||
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
|
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
|
||||||
|
| handleCollapsedChange | 表单收起展开状态变化回调 | `(collapsed: boolean) => void` | - |
|
||||||
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
|
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
|
||||||
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
||||||
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
|
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
|
||||||
|
|||||||
@ -47,7 +47,7 @@ async function handleSubmit(e: Event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = toRaw(await props.formApi.getValues());
|
const values = toRaw(await props.formApi.getValues()) ?? {};
|
||||||
await props.handleSubmit?.(values);
|
await props.handleSubmit?.(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ async function handleReset(e: Event) {
|
|||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
const props = unref(rootProps);
|
const props = unref(rootProps);
|
||||||
|
|
||||||
const values = toRaw(await props.formApi?.getValues());
|
const values = toRaw(await props.formApi?.getValues()) ?? {};
|
||||||
|
|
||||||
if (isFunction(props.handleReset)) {
|
if (isFunction(props.handleReset)) {
|
||||||
await props.handleReset?.(values);
|
await props.handleReset?.(values);
|
||||||
|
|||||||
@ -36,6 +36,7 @@ function getDefaultState(): VbenFormProps {
|
|||||||
handleReset: undefined,
|
handleReset: undefined,
|
||||||
handleSubmit: undefined,
|
handleSubmit: undefined,
|
||||||
handleValuesChange: undefined,
|
handleValuesChange: undefined,
|
||||||
|
handleCollapsedChange: undefined,
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
resetButtonOptions: {},
|
resetButtonOptions: {},
|
||||||
schema: [],
|
schema: [],
|
||||||
|
|||||||
@ -379,6 +379,10 @@ export interface VbenFormProps<
|
|||||||
* 表单字段映射
|
* 表单字段映射
|
||||||
*/
|
*/
|
||||||
fieldMappingTime?: FieldMappingTime;
|
fieldMappingTime?: FieldMappingTime;
|
||||||
|
/**
|
||||||
|
* 表单收起展开状态变化回调
|
||||||
|
*/
|
||||||
|
handleCollapsedChange?: (collapsed: boolean) => void;
|
||||||
/**
|
/**
|
||||||
* 表单重置回调
|
* 表单重置回调
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { useForm } from 'vee-validate';
|
|||||||
import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';
|
import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';
|
||||||
import { getDefaultsForSchema } from 'zod-defaults';
|
import { getDefaultsForSchema } from 'zod-defaults';
|
||||||
|
|
||||||
type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
|
type ExtendFormProps = VbenFormProps & { formApi?: ExtendedFormApi };
|
||||||
|
|
||||||
export const [injectFormProps, provideFormProps] =
|
export const [injectFormProps, provideFormProps] =
|
||||||
createContext<[ComputedRef<ExtendFormProps> | ExtendFormProps, FormActions]>(
|
createContext<[ComputedRef<ExtendFormProps> | ExtendFormProps, FormActions]>(
|
||||||
|
|||||||
@ -40,7 +40,9 @@ const { delegatedSlots, form } = useFormInitial(props);
|
|||||||
provideFormProps([props, form]);
|
provideFormProps([props, form]);
|
||||||
|
|
||||||
const handleUpdateCollapsed = (value: boolean) => {
|
const handleUpdateCollapsed = (value: boolean) => {
|
||||||
currentCollapsed.value = !!value;
|
currentCollapsed.value = value;
|
||||||
|
// 触发收起展开状态变化回调
|
||||||
|
props.handleCollapsedChange?.(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import {
|
|||||||
} from './use-form-context';
|
} from './use-form-context';
|
||||||
// 通过 extends 会导致热更新卡死,所以重复写了一遍
|
// 通过 extends 会导致热更新卡死,所以重复写了一遍
|
||||||
interface Props extends VbenFormProps {
|
interface Props extends VbenFormProps {
|
||||||
formApi: ExtendedFormApi;
|
formApi?: ExtendedFormApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
@ -44,11 +44,13 @@ provideComponentRefMap(componentRefMap);
|
|||||||
props.formApi?.mount?.(form, componentRefMap);
|
props.formApi?.mount?.(form, componentRefMap);
|
||||||
|
|
||||||
const handleUpdateCollapsed = (value: boolean) => {
|
const handleUpdateCollapsed = (value: boolean) => {
|
||||||
props.formApi?.setState({ collapsed: !!value });
|
props.formApi?.setState({ collapsed: value });
|
||||||
|
// 触发收起展开状态变化回调
|
||||||
|
forward.value.handleCollapsedChange?.(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleKeyDownEnter(event: KeyboardEvent) {
|
function handleKeyDownEnter(event: KeyboardEvent) {
|
||||||
if (!state.value.submitOnEnter || !forward.value.formApi?.isMounted) {
|
if (!state?.value.submitOnEnter || !forward.value.formApi?.isMounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 如果是 textarea 不阻止默认行为,否则会导致无法换行。
|
// 如果是 textarea 不阻止默认行为,否则会导致无法换行。
|
||||||
@ -58,11 +60,11 @@ function handleKeyDownEnter(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
forward.value.formApi.validateAndSubmitForm();
|
forward.value.formApi?.validateAndSubmitForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleValuesChangeDebounced = useDebounceFn(async () => {
|
const handleValuesChangeDebounced = useDebounceFn(async () => {
|
||||||
state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
|
state?.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
const valuesCache: Recordable<any> = {};
|
const valuesCache: Recordable<any> = {};
|
||||||
@ -74,7 +76,7 @@ onMounted(async () => {
|
|||||||
() => form.values,
|
() => form.values,
|
||||||
async (newVal) => {
|
async (newVal) => {
|
||||||
if (forward.value.handleValuesChange) {
|
if (forward.value.handleValuesChange) {
|
||||||
const fields = state.value.schema?.map((item) => {
|
const fields = state?.value.schema?.map((item) => {
|
||||||
return item.fieldName;
|
return item.fieldName;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,8 +93,9 @@ onMounted(async () => {
|
|||||||
|
|
||||||
if (changedFields.length > 0) {
|
if (changedFields.length > 0) {
|
||||||
// 调用handleValuesChange回调,传入所有表单值的深拷贝和变更的字段列表
|
// 调用handleValuesChange回调,传入所有表单值的深拷贝和变更的字段列表
|
||||||
|
const values = await forward.value.formApi?.getValues();
|
||||||
forward.value.handleValuesChange(
|
forward.value.handleValuesChange(
|
||||||
cloneDeep(await forward.value.formApi.getValues()),
|
cloneDeep(values ?? {}) as Record<string, any>,
|
||||||
changedFields,
|
changedFields,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,7 +112,7 @@ onMounted(async () => {
|
|||||||
<Form
|
<Form
|
||||||
@keydown.enter="handleKeyDownEnter"
|
@keydown.enter="handleKeyDownEnter"
|
||||||
v-bind="forward"
|
v-bind="forward"
|
||||||
:collapsed="state.collapsed"
|
:collapsed="state?.collapsed"
|
||||||
:component-bind-event-map="COMPONENT_BIND_EVENT_MAP"
|
:component-bind-event-map="COMPONENT_BIND_EVENT_MAP"
|
||||||
:component-map="COMPONENT_MAP"
|
:component-map="COMPONENT_MAP"
|
||||||
:form="form"
|
:form="form"
|
||||||
@ -126,7 +129,7 @@ onMounted(async () => {
|
|||||||
<slot v-bind="slotProps">
|
<slot v-bind="slotProps">
|
||||||
<FormActions
|
<FormActions
|
||||||
v-if="forward.showDefaultActions"
|
v-if="forward.showDefaultActions"
|
||||||
:model-value="state.collapsed"
|
:model-value="state?.collapsed"
|
||||||
@update:model-value="handleUpdateCollapsed"
|
@update:model-value="handleUpdateCollapsed"
|
||||||
>
|
>
|
||||||
<template #reset-before="resetSlotProps">
|
<template #reset-before="resetSlotProps">
|
||||||
|
|||||||
@ -30,6 +30,7 @@ describe('fileDownloader', () => {
|
|||||||
expect(result).toBeInstanceOf(Blob);
|
expect(result).toBeInstanceOf(Blob);
|
||||||
expect(result).toEqual(mockBlob);
|
expect(result).toEqual(mockBlob);
|
||||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
||||||
|
method: 'GET',
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
responseReturn: 'body',
|
responseReturn: 'body',
|
||||||
});
|
});
|
||||||
@ -51,6 +52,7 @@ describe('fileDownloader', () => {
|
|||||||
expect(result).toEqual(mockBlob);
|
expect(result).toEqual(mockBlob);
|
||||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
||||||
...customConfig,
|
...customConfig,
|
||||||
|
method: 'GET',
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
responseReturn: 'body',
|
responseReturn: 'body',
|
||||||
});
|
});
|
||||||
@ -84,3 +86,72 @@ describe('fileDownloader', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('fileDownloader use other method', () => {
|
||||||
|
let fileDownloader: FileDownloader;
|
||||||
|
|
||||||
|
it('should call request using get', async () => {
|
||||||
|
const url = 'https://example.com/file';
|
||||||
|
const mockBlob = new Blob(['file content'], { type: 'text/plain' });
|
||||||
|
const mockResponse: Blob = mockBlob;
|
||||||
|
|
||||||
|
const mockAxiosInstance = {
|
||||||
|
request: vi.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
fileDownloader = new FileDownloader(mockAxiosInstance);
|
||||||
|
|
||||||
|
mockAxiosInstance.request.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await fileDownloader.download(url);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(Blob);
|
||||||
|
expect(result).toEqual(mockBlob);
|
||||||
|
expect(mockAxiosInstance.request).toHaveBeenCalledWith(url, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
responseReturn: 'body',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call post', async () => {
|
||||||
|
const url = 'https://example.com/file';
|
||||||
|
|
||||||
|
const mockAxiosInstance = {
|
||||||
|
post: vi.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
fileDownloader = new FileDownloader(mockAxiosInstance);
|
||||||
|
|
||||||
|
const customConfig: AxiosRequestConfig = {
|
||||||
|
method: 'POST',
|
||||||
|
data: { name: 'aa' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await fileDownloader.download(url, customConfig);
|
||||||
|
|
||||||
|
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
|
||||||
|
url,
|
||||||
|
{ name: 'aa' },
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
responseType: 'blob',
|
||||||
|
responseReturn: 'body',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
const url = 'https://example.com/file';
|
||||||
|
const mockAxiosInstance = {
|
||||||
|
post: vi.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
fileDownloader = new FileDownloader(mockAxiosInstance);
|
||||||
|
await expect(() =>
|
||||||
|
fileDownloader.download(url, { method: 'postt' }),
|
||||||
|
).rejects.toThrow(
|
||||||
|
'RequestClient does not support method "POSTT". Please ensure the method is properly implemented in your RequestClient instance.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -28,13 +28,32 @@ class FileDownloader {
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const finalConfig: DownloadRequestConfig = {
|
const finalConfig: DownloadRequestConfig = {
|
||||||
responseReturn: 'body',
|
responseReturn: 'body',
|
||||||
|
method: 'GET',
|
||||||
...config,
|
...config,
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await this.client.get<T>(url, finalConfig);
|
// Prefer a generic request if available; otherwise, dispatch to method-specific calls.
|
||||||
|
const method = (finalConfig.method || 'GET').toUpperCase();
|
||||||
|
const clientAny = this.client as any;
|
||||||
|
|
||||||
return response;
|
if (typeof clientAny.request === 'function') {
|
||||||
|
return await clientAny.request(url, finalConfig);
|
||||||
|
}
|
||||||
|
const lower = method.toLowerCase();
|
||||||
|
|
||||||
|
if (typeof clientAny[lower] === 'function') {
|
||||||
|
if (['POST', 'PUT'].includes(method)) {
|
||||||
|
const { data, ...rest } = finalConfig as Record<string, any>;
|
||||||
|
return await clientAny[lower](url, data, rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await clientAny[lower](url, finalConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`RequestClient does not support method "${method}". Please ensure the method is properly implemented in your RequestClient instance.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user