feat: 添加完整的前端管理系统 (VbenAdmin)
- 添加基于 VbenAdmin + Vue3 + Element Plus 的前端管理系统 - 包含完整的 UI 组件库和工具链 - 支持多应用架构 (web-ele, backend-mock, playground) - 包含完整的开发规范和配置 - 修复 admin 目录的子模块问题,确保正确提交
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { FileDownloader } from './downloader';
|
||||
|
||||
describe('fileDownloader', () => {
|
||||
let fileDownloader: FileDownloader;
|
||||
const mockAxiosInstance = {
|
||||
get: vi.fn(),
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
fileDownloader = new FileDownloader(mockAxiosInstance);
|
||||
});
|
||||
|
||||
it('should create an instance of FileDownloader', () => {
|
||||
expect(fileDownloader).toBeInstanceOf(FileDownloader);
|
||||
});
|
||||
|
||||
it('should download a file and return a Blob', async () => {
|
||||
const url = 'https://example.com/file';
|
||||
const mockBlob = new Blob(['file content'], { type: 'text/plain' });
|
||||
const mockResponse: Blob = mockBlob;
|
||||
|
||||
mockAxiosInstance.get.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await fileDownloader.download(url);
|
||||
|
||||
expect(result).toBeInstanceOf(Blob);
|
||||
expect(result).toEqual(mockBlob);
|
||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
||||
responseType: 'blob',
|
||||
responseReturn: 'body',
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge provided config with default config', async () => {
|
||||
const url = 'https://example.com/file';
|
||||
const mockBlob = new Blob(['file content'], { type: 'text/plain' });
|
||||
const mockResponse: Blob = mockBlob;
|
||||
|
||||
mockAxiosInstance.get.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const customConfig: AxiosRequestConfig = {
|
||||
headers: { 'Custom-Header': 'value' },
|
||||
};
|
||||
|
||||
const result = await fileDownloader.download(url, customConfig);
|
||||
expect(result).toBeInstanceOf(Blob);
|
||||
expect(result).toEqual(mockBlob);
|
||||
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
|
||||
...customConfig,
|
||||
responseType: 'blob',
|
||||
responseReturn: 'body',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
const url = 'https://example.com/file';
|
||||
mockAxiosInstance.get.mockRejectedValueOnce(new Error('Network Error'));
|
||||
await expect(fileDownloader.download(url)).rejects.toThrow('Network Error');
|
||||
});
|
||||
|
||||
it('should handle empty URL gracefully', async () => {
|
||||
const url = '';
|
||||
mockAxiosInstance.get.mockRejectedValueOnce(
|
||||
new Error('Request failed with status code 404'),
|
||||
);
|
||||
|
||||
await expect(fileDownloader.download(url)).rejects.toThrow(
|
||||
'Request failed with status code 404',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle null URL gracefully', async () => {
|
||||
const url = null as unknown as string;
|
||||
mockAxiosInstance.get.mockRejectedValueOnce(
|
||||
new Error('Request failed with status code 404'),
|
||||
);
|
||||
|
||||
await expect(fileDownloader.download(url)).rejects.toThrow(
|
||||
'Request failed with status code 404',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { RequestClient } from '../request-client';
|
||||
import type { RequestClientConfig } from '../types';
|
||||
|
||||
type DownloadRequestConfig = {
|
||||
/**
|
||||
* 定义期望获得的数据类型。
|
||||
* raw: 原始的AxiosResponse,包括headers、status等。
|
||||
* body: 只返回响应数据的BODY部分(Blob)
|
||||
*/
|
||||
responseReturn?: 'body' | 'raw';
|
||||
} & Omit<RequestClientConfig, 'responseReturn'>;
|
||||
|
||||
class FileDownloader {
|
||||
private client: RequestClient;
|
||||
|
||||
constructor(client: RequestClient) {
|
||||
this.client = client;
|
||||
}
|
||||
/**
|
||||
* 下载文件
|
||||
* @param url 文件的完整链接
|
||||
* @param config 配置信息,可选。
|
||||
* @returns 如果config.responseReturn为'body',则返回Blob(默认),否则返回RequestResponse<Blob>
|
||||
*/
|
||||
public async download<T = Blob>(
|
||||
url: string,
|
||||
config?: DownloadRequestConfig,
|
||||
): Promise<T> {
|
||||
const finalConfig: DownloadRequestConfig = {
|
||||
responseReturn: 'body',
|
||||
...config,
|
||||
responseType: 'blob',
|
||||
};
|
||||
|
||||
const response = await this.client.get<T>(url, finalConfig);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export { FileDownloader };
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { AxiosInstance, AxiosResponse } from 'axios';
|
||||
|
||||
import type {
|
||||
RequestInterceptorConfig,
|
||||
ResponseInterceptorConfig,
|
||||
} from '../types';
|
||||
|
||||
const defaultRequestInterceptorConfig: RequestInterceptorConfig = {
|
||||
fulfilled: (response) => response,
|
||||
rejected: (error) => Promise.reject(error),
|
||||
};
|
||||
|
||||
const defaultResponseInterceptorConfig: ResponseInterceptorConfig = {
|
||||
fulfilled: (response: AxiosResponse) => response,
|
||||
rejected: (error) => Promise.reject(error),
|
||||
};
|
||||
|
||||
class InterceptorManager {
|
||||
private axiosInstance: AxiosInstance;
|
||||
|
||||
constructor(instance: AxiosInstance) {
|
||||
this.axiosInstance = instance;
|
||||
}
|
||||
|
||||
addRequestInterceptor({
|
||||
fulfilled,
|
||||
rejected,
|
||||
}: RequestInterceptorConfig = defaultRequestInterceptorConfig) {
|
||||
this.axiosInstance.interceptors.request.use(fulfilled, rejected);
|
||||
}
|
||||
|
||||
addResponseInterceptor<T = any>({
|
||||
fulfilled,
|
||||
rejected,
|
||||
}: ResponseInterceptorConfig<T> = defaultResponseInterceptorConfig) {
|
||||
this.axiosInstance.interceptors.response.use(fulfilled, rejected);
|
||||
}
|
||||
}
|
||||
|
||||
export { InterceptorManager };
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { FileUploader } from './uploader';
|
||||
|
||||
describe('fileUploader', () => {
|
||||
let fileUploader: FileUploader;
|
||||
// Mock the AxiosInstance
|
||||
const mockAxiosInstance = {
|
||||
post: vi.fn(),
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
fileUploader = new FileUploader(mockAxiosInstance);
|
||||
});
|
||||
|
||||
it('should create an instance of FileUploader', () => {
|
||||
expect(fileUploader).toBeInstanceOf(FileUploader);
|
||||
});
|
||||
|
||||
it('should upload a file and return the response', async () => {
|
||||
const url = 'https://example.com/upload';
|
||||
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
|
||||
const mockResponse: AxiosResponse = {
|
||||
config: {} as any,
|
||||
data: { success: true },
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
(
|
||||
mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>
|
||||
).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await fileUploader.upload(url, { file });
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
|
||||
url,
|
||||
expect.any(FormData),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge provided config with default config', async () => {
|
||||
const url = 'https://example.com/upload';
|
||||
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
|
||||
const mockResponse: AxiosResponse = {
|
||||
config: {} as any,
|
||||
data: { success: true },
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
};
|
||||
|
||||
(
|
||||
mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>
|
||||
).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const customConfig: AxiosRequestConfig = {
|
||||
headers: { 'Custom-Header': 'value' },
|
||||
};
|
||||
|
||||
const result = await fileUploader.upload(url, { file }, customConfig);
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
|
||||
url,
|
||||
expect.any(FormData),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Custom-Header': 'value',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
const url = 'https://example.com/upload';
|
||||
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
|
||||
(
|
||||
mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>
|
||||
).mockRejectedValueOnce(new Error('Network Error'));
|
||||
|
||||
await expect(fileUploader.upload(url, { file })).rejects.toThrow(
|
||||
'Network Error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty URL gracefully', async () => {
|
||||
const url = '';
|
||||
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
|
||||
(
|
||||
mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>
|
||||
).mockRejectedValueOnce(new Error('Request failed with status code 404'));
|
||||
|
||||
await expect(fileUploader.upload(url, { file })).rejects.toThrow(
|
||||
'Request failed with status code 404',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle null URL gracefully', async () => {
|
||||
const url = null as unknown as string;
|
||||
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
|
||||
(
|
||||
mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>
|
||||
).mockRejectedValueOnce(new Error('Request failed with status code 404'));
|
||||
|
||||
await expect(fileUploader.upload(url, { file })).rejects.toThrow(
|
||||
'Request failed with status code 404',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { RequestClient } from '../request-client';
|
||||
import type { RequestClientConfig } from '../types';
|
||||
|
||||
import { isUndefined } from '@vben/utils';
|
||||
|
||||
class FileUploader {
|
||||
private client: RequestClient;
|
||||
|
||||
constructor(client: RequestClient) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public async upload<T = any>(
|
||||
url: string,
|
||||
data: Record<string, any> & { file: Blob | File },
|
||||
config?: RequestClientConfig,
|
||||
): Promise<T> {
|
||||
const formData = new FormData();
|
||||
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => {
|
||||
!isUndefined(item) && formData.append(`${key}[${index}]`, item);
|
||||
});
|
||||
} else {
|
||||
!isUndefined(value) && formData.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
const finalConfig: RequestClientConfig = {
|
||||
...config,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
...config?.headers,
|
||||
},
|
||||
};
|
||||
|
||||
return this.client.post(url, formData, finalConfig);
|
||||
}
|
||||
}
|
||||
|
||||
export { FileUploader };
|
||||
Reference in New Issue
Block a user