Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
@@ -71,17 +71,10 @@ const modelValue = defineModel({ default: '', type: String });
|
||||
|
||||
const visible = ref(false);
|
||||
const currentSelect = ref('');
|
||||
const currentPage = ref(1);
|
||||
const keyword = ref('');
|
||||
const keywordDebounce = refDebounced(keyword, 300);
|
||||
const innerIcons = ref<string[]>([]);
|
||||
|
||||
/* 当检索关键词变化时,重置分页 */
|
||||
watch(keywordDebounce, () => {
|
||||
currentPage.value = 1;
|
||||
setCurrentPage(1);
|
||||
});
|
||||
|
||||
watchDebounced(
|
||||
() => props.prefix,
|
||||
async (prefix) => {
|
||||
@@ -122,7 +115,7 @@ const showList = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const { paginationList, total, setCurrentPage } = usePagination(
|
||||
const { paginationList, total, setCurrentPage, currentPage } = usePagination(
|
||||
showList,
|
||||
props.pageSize,
|
||||
);
|
||||
@@ -145,7 +138,6 @@ const handleClick = (icon: string) => {
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
currentPage.value = page;
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
|
||||
@@ -55,16 +55,12 @@ function handleClick(event: MouseEvent) {
|
||||
return;
|
||||
}
|
||||
const param: JsonViewerValue = {
|
||||
path: '',
|
||||
value: '',
|
||||
depth: 0,
|
||||
el: event.target,
|
||||
path: pathNode.getAttribute('path') || '',
|
||||
depth: Number(pathNode.getAttribute('depth')) || 0,
|
||||
value: event.target.textContent || undefined,
|
||||
};
|
||||
|
||||
param.path = pathNode.getAttribute('path') || '';
|
||||
param.depth = Number(pathNode.getAttribute('depth')) || 0;
|
||||
|
||||
param.value = event.target.textContent || undefined;
|
||||
param.value = JSON.parse(param.value);
|
||||
emit('valueClick', param);
|
||||
}
|
||||
|
||||
@@ -618,13 +618,11 @@ const stickStyles = computed(() => (stick: string) => {
|
||||
const stickStyle = {
|
||||
width: `${stickSize.value / parentScaleX.value}px`,
|
||||
height: `${stickSize.value / parentScaleY.value}px`,
|
||||
[styleMapping.y[stick[0] as 'b' | 'm' | 't'] as 'height' | 'width']:
|
||||
`${stickSize.value / parentScaleX.value / -2}px`,
|
||||
[styleMapping.x[stick[1] as 'l' | 'm' | 'r'] as 'height' | 'width']:
|
||||
`${stickSize.value / parentScaleX.value / -2}px`,
|
||||
};
|
||||
stickStyle[
|
||||
styleMapping.y[stick[0] as 'b' | 'm' | 't'] as 'height' | 'width'
|
||||
] = `${stickSize.value / parentScaleX.value / -2}px`;
|
||||
stickStyle[
|
||||
styleMapping.x[stick[1] as 'l' | 'm' | 'r'] as 'height' | 'width'
|
||||
] = `${stickSize.value / parentScaleX.value / -2}px`;
|
||||
return stickStyle;
|
||||
});
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './about';
|
||||
export * from './authentication';
|
||||
export * from './dashboard';
|
||||
export * from './fallback';
|
||||
export * from './profile';
|
||||
|
||||
56
packages/effects/common-ui/src/ui/profile/base-setting.vue
Normal file
56
packages/effects/common-ui/src/ui/profile/base-setting.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VbenFormSchema } from '@vben-core/form-ui';
|
||||
|
||||
import { computed, reactive } from 'vue';
|
||||
|
||||
import { useVbenForm } from '@vben-core/form-ui';
|
||||
import { VbenButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
formSchema?: VbenFormSchema[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
formSchema: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [Recordable<any>];
|
||||
}>();
|
||||
|
||||
const [Form, formApi] = useVbenForm(
|
||||
reactive({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: computed(() => props.formSchema),
|
||||
showDefaultActions: false,
|
||||
}),
|
||||
);
|
||||
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formApi.validate();
|
||||
const values = await formApi.getValues();
|
||||
if (valid) {
|
||||
emit('submit', values);
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getFormApi: () => formApi,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div @keydown.enter.prevent="handleSubmit">
|
||||
<Form />
|
||||
<VbenButton type="submit" class="mt-4" @click="handleSubmit">
|
||||
更新基本信息
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
6
packages/effects/common-ui/src/ui/profile/index.ts
Normal file
6
packages/effects/common-ui/src/ui/profile/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as ProfileBaseSetting } from './base-setting.vue';
|
||||
export { default as ProfileNotificationSetting } from './notification-setting.vue';
|
||||
export { default as ProfilePasswordSetting } from './password-setting.vue';
|
||||
export { default as Profile } from './profile.vue';
|
||||
export { default as ProfileSecuritySetting } from './security-setting.vue';
|
||||
export type * from './types';
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { SettingProps } from './types';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
Switch,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
withDefaults(defineProps<SettingProps>(), {
|
||||
formSchema: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [Recordable<any>];
|
||||
}>();
|
||||
|
||||
function handleChange(fieldName: string, value: boolean) {
|
||||
emit('change', { fieldName, value });
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Form class="space-y-8">
|
||||
<div class="space-y-4">
|
||||
<template v-for="item in formSchema" :key="item.fieldName">
|
||||
<FormField type="checkbox" :name="item.fieldName">
|
||||
<FormItem
|
||||
class="flex flex-row items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<div class="space-y-0.5">
|
||||
<FormLabel class="text-base"> {{ item.label }} </FormLabel>
|
||||
<FormDescription>
|
||||
{{ item.description }}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
:model-value="item.value"
|
||||
@update:model-value="handleChange(item.fieldName, $event)"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VbenFormSchema } from '@vben-core/form-ui';
|
||||
|
||||
import { computed, reactive } from 'vue';
|
||||
|
||||
import { useVbenForm } from '@vben-core/form-ui';
|
||||
import { VbenButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
formSchema?: VbenFormSchema[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
formSchema: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [Recordable<any>];
|
||||
}>();
|
||||
|
||||
const [Form, formApi] = useVbenForm(
|
||||
reactive({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: computed(() => props.formSchema),
|
||||
showDefaultActions: false,
|
||||
}),
|
||||
);
|
||||
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formApi.validate();
|
||||
const values = await formApi.getValues();
|
||||
if (valid) {
|
||||
emit('submit', values);
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getFormApi: () => formApi,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<Form />
|
||||
<VbenButton type="submit" class="mt-4" @click="handleSubmit">
|
||||
更新密码
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
62
packages/effects/common-ui/src/ui/profile/profile.vue
Normal file
62
packages/effects/common-ui/src/ui/profile/profile.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import type { Props } from './types';
|
||||
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
import {
|
||||
Card,
|
||||
Separator,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
VbenAvatar,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
import { Page } from '../../components';
|
||||
|
||||
defineOptions({
|
||||
name: 'ProfileUI',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
title: '关于项目',
|
||||
tabs: () => [],
|
||||
});
|
||||
|
||||
const tabsValue = defineModel<string>('modelValue');
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="flex h-full w-full">
|
||||
<Card class="w-1/6 flex-none">
|
||||
<div class="mt-4 flex h-40 flex-col items-center justify-center gap-4">
|
||||
<VbenAvatar
|
||||
:src="userInfo?.avatar ?? preferences.app.defaultAvatar"
|
||||
class="size-20"
|
||||
/>
|
||||
<span class="text-lg font-semibold">
|
||||
{{ userInfo?.realName ?? '' }}
|
||||
</span>
|
||||
<span class="text-foreground/80 text-sm">
|
||||
{{ userInfo?.username ?? '' }}
|
||||
</span>
|
||||
</div>
|
||||
<Separator class="my-4" />
|
||||
<Tabs v-model="tabsValue" orientation="vertical" class="m-4">
|
||||
<TabsList class="bg-card grid w-full grid-cols-1">
|
||||
<TabsTrigger
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
:value="tab.value"
|
||||
class="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground h-12 justify-start"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</Card>
|
||||
<Card class="ml-4 w-5/6 flex-auto p-8">
|
||||
<slot name="content"></slot>
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { SettingProps } from './types';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
Switch,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
withDefaults(defineProps<SettingProps>(), {
|
||||
formSchema: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [Recordable<any>];
|
||||
}>();
|
||||
|
||||
function handleChange(fieldName: string, value: boolean) {
|
||||
emit('change', { fieldName, value });
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Form class="space-y-8">
|
||||
<div class="space-y-4">
|
||||
<template v-for="item in formSchema" :key="item.fieldName">
|
||||
<FormField type="checkbox" :name="item.fieldName">
|
||||
<FormItem
|
||||
class="flex flex-row items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<div class="space-y-0.5">
|
||||
<FormLabel class="text-base"> {{ item.label }} </FormLabel>
|
||||
<FormDescription>
|
||||
{{ item.description }}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
:model-value="item.value"
|
||||
@update:model-value="handleChange(item.fieldName, $event)"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
21
packages/effects/common-ui/src/ui/profile/types.ts
Normal file
21
packages/effects/common-ui/src/ui/profile/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { BasicUserInfo } from '@vben/types';
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
userInfo: BasicUserInfo | null;
|
||||
tabs: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface FormSchemaItem {
|
||||
description: string;
|
||||
fieldName: string;
|
||||
label: string;
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
export interface SettingProps {
|
||||
formSchema: FormSchemaItem[];
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { computed, ref, unref } from 'vue';
|
||||
import { computed, ref, unref, watch } from 'vue';
|
||||
|
||||
/**
|
||||
* Paginates an array of items
|
||||
@@ -22,7 +22,11 @@ function pagination<T = any>(list: T[], pageNo: number, pageSize: number): T[] {
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {
|
||||
export function usePagination<T = any>(
|
||||
list: Ref<T[]>,
|
||||
pageSize: number,
|
||||
totalChangeToFirstPage = true,
|
||||
) {
|
||||
const currentPage = ref(1);
|
||||
const pageSizeRef = ref(pageSize);
|
||||
|
||||
@@ -38,11 +42,21 @@ export function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {
|
||||
return unref(list).length;
|
||||
});
|
||||
|
||||
if (totalChangeToFirstPage) {
|
||||
watch(total, () => {
|
||||
setCurrentPage(1);
|
||||
});
|
||||
}
|
||||
|
||||
function setCurrentPage(page: number) {
|
||||
if (page < 1 || page > unref(totalPages)) {
|
||||
throw new Error('Invalid page number');
|
||||
if (page === 1 && unref(totalPages) === 0) {
|
||||
currentPage.value = 1;
|
||||
} else {
|
||||
if (page < 1 || page > unref(totalPages)) {
|
||||
throw new Error('Invalid page number');
|
||||
}
|
||||
currentPage.value = page;
|
||||
}
|
||||
currentPage.value = page;
|
||||
}
|
||||
|
||||
function setPageSize(pageSize: number) {
|
||||
@@ -54,5 +68,5 @@ export function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {
|
||||
currentPage.value = 1;
|
||||
}
|
||||
|
||||
return { setCurrentPage, total, setPageSize, paginationList };
|
||||
return { setCurrentPage, total, setPageSize, paginationList, currentPage };
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ const rightSlots = computed(() => {
|
||||
list.push({ index: Number(name[2]), name: key });
|
||||
}
|
||||
});
|
||||
return list.sort((a, b) => a.index - b.index);
|
||||
return list.toSorted((a, b) => a.index - b.index);
|
||||
});
|
||||
|
||||
const leftSlots = computed(() => {
|
||||
@@ -111,7 +111,7 @@ const leftSlots = computed(() => {
|
||||
list.push({ index: Number(name[2]), name: key });
|
||||
}
|
||||
});
|
||||
return list.sort((a, b) => a.index - b.index);
|
||||
return list.toSorted((a, b) => a.index - b.index);
|
||||
});
|
||||
|
||||
function clearPreferencesAndLogout() {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NotificationItem } from './types';
|
||||
|
||||
import { Bell, MailCheck } from '@vben/icons';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Bell, CircleCheckBig, CircleX, MailCheck } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
@@ -36,9 +38,11 @@ const emit = defineEmits<{
|
||||
makeAll: [];
|
||||
open: [boolean];
|
||||
read: [NotificationItem];
|
||||
remove: [NotificationItem];
|
||||
viewAll: [];
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const [open, toggle] = useToggle();
|
||||
|
||||
function close() {
|
||||
@@ -59,7 +63,28 @@ function handleClear() {
|
||||
}
|
||||
|
||||
function handleClick(item: NotificationItem) {
|
||||
emit('read', item);
|
||||
// 如果通知项有链接,点击时跳转
|
||||
if (item.link) {
|
||||
navigateTo(item.link, item.query, item.state);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(
|
||||
link: string,
|
||||
query?: Record<string, any>,
|
||||
state?: Record<string, any>,
|
||||
) {
|
||||
if (link.startsWith('http://') || link.startsWith('https://')) {
|
||||
// 外部链接,在新标签页打开
|
||||
window.open(link, '_blank');
|
||||
} else {
|
||||
// 内部路由链接,支持 query 参数和 state
|
||||
router.push({
|
||||
path: link,
|
||||
query: query || {},
|
||||
state,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
@@ -97,7 +122,7 @@ function handleOpen() {
|
||||
</div>
|
||||
<VbenScrollbar v-if="notifications.length > 0">
|
||||
<ul class="!flex max-h-[360px] w-full flex-col">
|
||||
<template v-for="item in notifications" :key="item.title">
|
||||
<template v-for="item in notifications" :key="item.id ?? item.title">
|
||||
<li
|
||||
class="hover:bg-accent border-border relative flex w-full cursor-pointer items-start gap-5 border-t px-3 py-3"
|
||||
@click="handleClick(item)"
|
||||
@@ -113,7 +138,6 @@ function handleOpen() {
|
||||
<img
|
||||
:src="item.avatar"
|
||||
class="aspect-square h-full w-full object-cover"
|
||||
role="img"
|
||||
/>
|
||||
</span>
|
||||
<div class="flex flex-col gap-1 leading-none">
|
||||
@@ -125,6 +149,30 @@ function handleOpen() {
|
||||
{{ item.date }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="absolute right-3 top-1/2 flex -translate-y-1/2 flex-col gap-2"
|
||||
>
|
||||
<VbenIconButton
|
||||
v-if="!item.isRead"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
class="h-6 px-2"
|
||||
:tooltip="$t('common.confirm')"
|
||||
@click.stop="emit('read', item)"
|
||||
>
|
||||
<CircleCheckBig class="size-4" />
|
||||
</VbenIconButton>
|
||||
<VbenIconButton
|
||||
v-if="item.isRead"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
class="text-destructive h-6 px-2"
|
||||
:tooltip="$t('common.delete')"
|
||||
@click.stop="emit('remove', item)"
|
||||
>
|
||||
<CircleX class="size-4" />
|
||||
</VbenIconButton>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
interface NotificationItem {
|
||||
id: any;
|
||||
avatar: string;
|
||||
date: string;
|
||||
isRead?: boolean;
|
||||
message: string;
|
||||
title: string;
|
||||
id?: number;
|
||||
/**
|
||||
* 跳转链接,可以是路由路径或完整 URL
|
||||
* @example '/dashboard' 或 'https://example.com'
|
||||
*/
|
||||
link?: string;
|
||||
query?: Record<string, any>;
|
||||
state?: Record<string, any>;
|
||||
}
|
||||
|
||||
export type { NotificationItem };
|
||||
|
||||
@@ -66,7 +66,7 @@ function toggleTheme(event: MouseEvent) {
|
||||
];
|
||||
const animate = document.documentElement.animate(
|
||||
{
|
||||
clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
|
||||
clipPath: isDark.value ? [...clipPath].toReversed() : clipPath,
|
||||
},
|
||||
{
|
||||
duration: 450,
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('fileDownloader', () => {
|
||||
expect(result).toBeInstanceOf(Blob);
|
||||
expect(result).toEqual(mockBlob);
|
||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
responseReturn: 'body',
|
||||
});
|
||||
@@ -51,6 +52,7 @@ describe('fileDownloader', () => {
|
||||
expect(result).toEqual(mockBlob);
|
||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
||||
...customConfig,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
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> {
|
||||
const finalConfig: DownloadRequestConfig = {
|
||||
responseReturn: 'body',
|
||||
method: 'GET',
|
||||
...config,
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user