Browse Source

feat: refactor request

vben 9 months ago
parent
commit
f95cc80895
29 changed files with 1096 additions and 265 deletions
  1. 2 2
      .vscode/settings.json
  2. 1 1
      apps/antd-view/package.json
  3. 1 1
      apps/antd-view/src/apis/modules/user.ts
  4. 0 153
      apps/antd-view/src/apis/request.ts
  5. 81 0
      apps/antd-view/src/forward/request.ts
  6. 1 1
      apps/antd-view/src/views/_essential/authentication/login.vue
  7. 1 1
      package.json
  8. 0 0
      packages/@vben-core/forward/request/build.config.ts
  9. 7 2
      packages/@vben-core/forward/request/package.json
  10. 3 0
      packages/@vben-core/forward/request/src/index.ts
  11. 3 0
      packages/@vben-core/forward/request/src/request-client/index.ts
  12. 127 0
      packages/@vben-core/forward/request/src/request-client/modules/canceler.test.ts
  13. 52 0
      packages/@vben-core/forward/request/src/request-client/modules/canceler.ts
  14. 84 0
      packages/@vben-core/forward/request/src/request-client/modules/downloader.test.ts
  15. 30 0
      packages/@vben-core/forward/request/src/request-client/modules/downloader.ts
  16. 33 0
      packages/@vben-core/forward/request/src/request-client/modules/interceptor.ts
  17. 118 0
      packages/@vben-core/forward/request/src/request-client/modules/uploader.test.ts
  18. 32 0
      packages/@vben-core/forward/request/src/request-client/modules/uploader.ts
  19. 97 0
      packages/@vben-core/forward/request/src/request-client/request-client.test.ts
  20. 179 0
      packages/@vben-core/forward/request/src/request-client/request-client.ts
  21. 24 0
      packages/@vben-core/forward/request/src/request-client/types.ts
  22. 25 0
      packages/@vben-core/forward/request/src/request-client/util.test.ts
  23. 7 0
      packages/@vben-core/forward/request/src/request-client/util.ts
  24. 0 0
      packages/@vben-core/forward/request/src/use-request.ts
  25. 0 0
      packages/@vben-core/forward/request/tsconfig.json
  26. 0 1
      packages/request/src/index.ts
  27. 51 0
      packages/styles/src/common/entry.scss
  28. 133 99
      pnpm-lock.yaml
  29. 4 4
      vben-admin.code-workspace

+ 2 - 2
.vscode/settings.json

@@ -195,8 +195,8 @@
   "explorer.fileNesting.enabled": true,
   "explorer.fileNesting.expand": false,
   "explorer.fileNesting.patterns": {
-    "*.ts": "$(capture).test.ts, $(capture).test.tsx",
-    "*.tsx": "$(capture).test.ts, $(capture).test.tsx",
+    "*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts",
+    "*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts",
     "*.env": "$(capture).env.*",
     "README.md": "README*,CHANGELOG*,LICENSE,CNAME",
     "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",

+ 1 - 1
apps/antd-view/package.json

@@ -24,6 +24,7 @@
   "dependencies": {
     "@vben-core/helpers": "workspace:*",
     "@vben-core/preferences": "workspace:*",
+    "@vben-core/request": "workspace:*",
     "@vben-core/stores": "workspace:*",
     "@vben/common-ui": "workspace:*",
     "@vben/constants": "workspace:*",
@@ -31,7 +32,6 @@
     "@vben/icons": "workspace:*",
     "@vben/layouts": "workspace:*",
     "@vben/locales": "workspace:*",
-    "@vben/request": "workspace:*",
     "@vben/styles": "workspace:*",
     "@vben/types": "workspace:*",
     "@vben/utils": "workspace:*",

+ 1 - 1
apps/antd-view/src/apis/modules/user.ts

@@ -1,7 +1,7 @@
 import type { UserApiType } from '@/apis/types';
 import type { UserInfo } from '@vben/types';
 
-import { request } from '@/apis/request';
+import { request } from '@/forward/request';
 
 /**
  * 登录

+ 0 - 153
apps/antd-view/src/apis/request.ts

@@ -1,153 +0,0 @@
-/**
- * 该文件可自行根据业务逻辑进行调整
- */
-
-import { useAccessStore } from '@vben-core/stores';
-
-import { message } from 'ant-design-vue';
-import axios, {
-  AxiosError,
-  AxiosRequestConfig,
-  AxiosResponse,
-  InternalAxiosRequestConfig,
-} from 'axios';
-
-// 后端需要的 token 存放在header内的key字段
-// 可以根据自己的需要修改
-const REQUEST_HEADER_TOKEN_KEY = 'Authorization';
-
-type HttpConfig = InternalAxiosRequestConfig;
-
-interface HttpResponse<T = any> {
-  code: number;
-  message: string;
-  result: T;
-}
-
-// 用于存储每个请求的标识和取消函数
-const pendingMap = new Map<string, AbortController>();
-
-const getPendingUrl = (config: AxiosRequestConfig): string => {
-  return [config.method, config.url].join('&');
-};
-
-/**
- * 添加请求
- * @param config 请求配置
- */
-function addRequestSignal(config: AxiosRequestConfig): void {
-  abortRequest(config);
-  const url = getPendingUrl(config);
-  const controller = new AbortController();
-  config.signal = config.signal || controller.signal;
-  if (!pendingMap.has(url)) {
-    // 如果当前请求不在等待中,将其添加到等待中
-    pendingMap.set(url, controller);
-  }
-}
-
-/**
- * 清除所有等待中的请求
- */
-function abortAllRequest() {
-  pendingMap.forEach((abortController) => {
-    if (abortController) {
-      abortController.abort();
-    }
-  });
-  pendingMap.clear();
-}
-
-/**
- * 移除请求
- * @param config 请求配置
- */
-function abortRequest(config: AxiosRequestConfig): void {
-  if (!config) {
-    return;
-  }
-  const url = getPendingUrl(config);
-  if (pendingMap.has(url)) {
-    // 如果当前请求在等待中,取消它并将其从等待中移除
-    const abortController = pendingMap.get(url);
-    if (abortController) {
-      abortController.abort(url);
-    }
-    pendingMap.delete(url);
-  }
-}
-
-const axiosInstance = axios.create({
-  // .env 环境获取请求地址
-  baseURL: import.meta.env.VITE_GLOB_API_URL,
-  headers: {
-    'Content-Type': 'application/json;charset=utf-8',
-  },
-  timeout: 10 * 1000,
-});
-
-// 请求拦截器
-axiosInstance.interceptors.request.use(
-  (config: HttpConfig) => {
-    addRequestSignal(config);
-
-    // 携带 getAccessToken 在请求头
-    const accessStore = useAccessStore();
-    const getAccessToken = accessStore.getAccessToken;
-
-    if (getAccessToken) {
-      config.headers[REQUEST_HEADER_TOKEN_KEY] = getAccessToken;
-    }
-    return config;
-  },
-  (error: AxiosError) => {
-    return Promise.reject(error);
-  },
-);
-
-// 添加响应拦截器
-axiosInstance.interceptors.response.use(
-  (response: AxiosResponse<HttpResponse>) => {
-    const { data: responseData, status } = response;
-    const { code, message: msg, result } = responseData;
-    abortRequest(response.config);
-
-    if (status === 200 && code === 0) {
-      return result;
-    } else {
-      message.error(msg);
-      throw new Error(msg);
-    }
-  },
-  (error: any) => {
-    if (axios.isCancel(error)) {
-      return Promise.reject(error);
-    }
-
-    const err: string = error?.toString?.() ?? '';
-    let errMsg = '';
-    if (err?.includes('Network Error')) {
-      errMsg = '网络错误。';
-    } else if (error?.message?.includes?.('timeout')) {
-      errMsg = '请求超时。';
-    } else {
-      errMsg = error?.response?.data?.error?.message ?? '';
-    }
-    message.error(errMsg);
-    return Promise.reject(error);
-  },
-);
-
-async function request<T>(url: string, config: AxiosRequestConfig): Promise<T> {
-  try {
-    const response: AxiosResponse<T> = await axiosInstance({
-      url,
-      ...config,
-    });
-    return response as T;
-  } catch (error: any) {
-    throw error.response ? error.response.data : error;
-  }
-}
-
-export { abortAllRequest, request };

+ 81 - 0
apps/antd-view/src/forward/request.ts

@@ -0,0 +1,81 @@
+/**
+ * 该文件可自行根据业务逻辑进行调整
+ */
+
+import type { AxiosResponse } from '@vben-core/request';
+
+import { RequestClient, isCancelError } from '@vben-core/request';
+import { useAccessStore } from '@vben-core/stores';
+
+import { message } from 'ant-design-vue';
+
+interface HttpResponse<T = any> {
+  code: number;
+  message: string;
+  result: T;
+}
+
+function createRequestClient() {
+  const client = new RequestClient({
+    baseURL: import.meta.env.VITE_GLOB_API_URL,
+    // 为每个请求携带 Authorization
+    makeAuthorization: () => {
+      return {
+        handle: () => {
+          const accessStore = useAccessStore();
+          return accessStore.getAccessToken;
+        },
+        // 默认
+        // key: 'Authorization',
+      };
+    },
+  });
+  setupRequestInterceptors(client);
+  const request = client.request.bind(client);
+  const get = client.get.bind(client);
+  const post = client.post.bind(client);
+  return {
+    get,
+    post,
+    request,
+  };
+}
+
+function setupRequestInterceptors(client: RequestClient) {
+  client.addResponseInterceptor(
+    (response: AxiosResponse<HttpResponse>) => {
+      const { data: responseData, status } = response;
+      const { code, message: msg, result } = responseData;
+      if (status === 200 && code === 0) {
+        return result;
+      } else {
+        message.error(msg);
+        throw new Error(msg);
+      }
+    },
+    (error: any) => {
+      if (isCancelError(error)) {
+        return Promise.reject(error);
+      }
+
+      const err: string = error?.toString?.() ?? '';
+      let errMsg = '';
+      if (err?.includes('Network Error')) {
+        errMsg = '网络错误。';
+      } else if (error?.message?.includes?.('timeout')) {
+        errMsg = '请求超时。';
+      } else {
+        errMsg = error?.response?.data?.error?.message ?? '';
+      }
+      message.error(errMsg);
+      return Promise.reject(error);
+    },
+  );
+}
+
+const { request } = createRequestClient();
+
+// 其他配置的请求方法
+// const { request: xxxRequest } = createRequest();
+
+export { request };

+ 1 - 1
apps/antd-view/src/views/_essential/authentication/login.vue

@@ -1,12 +1,12 @@
 <script lang="ts" setup>
 import type { LoginAndRegisterParams } from '@vben/common-ui';
 
+import { useRequest } from '@vben-core/request';
 import { useAccessStore } from '@vben-core/stores';
 
 import { getUserInfo, userLogin } from '@/apis';
 import { AuthenticationLogin } from '@vben/common-ui';
 import { $t } from '@vben/locales';
-import { useRequest } from '@vben/request';
 import { notification } from 'ant-design-vue';
 import { computed } from 'vue';
 import { useRouter } from 'vue-router';

+ 1 - 1
package.json

@@ -47,7 +47,7 @@
     "@changesets/cli": "^2.27.5",
     "@ls-lint/ls-lint": "^2.2.3",
     "@types/jsdom": "^21.1.7",
-    "@types/node": "^20.13.0",
+    "@types/node": "^20.14.0",
     "@vben/commitlint-config": "workspace:*",
     "@vben/eslint-config": "workspace:*",
     "@vben/lint-staged-config": "workspace:*",

+ 0 - 0
packages/request/build.config.ts → packages/@vben-core/forward/request/build.config.ts


+ 7 - 2
packages/request/package.json → packages/@vben-core/forward/request/package.json

@@ -1,5 +1,5 @@
 {
-  "name": "@vben/request",
+  "name": "@vben-core/request",
   "version": "1.0.0",
   "type": "module",
   "license": "MIT",
@@ -7,7 +7,7 @@
   "repository": {
     "type": "git",
     "url": "git+https://github.com/vbenjs/vue-vben-admin.git",
-    "directory": "packages/request"
+    "directory": "packages/@vben-core/forward/request"
   },
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "scripts": {
@@ -41,6 +41,11 @@
     }
   },
   "dependencies": {
+    "@vben-core/toolkit": "workspace:*",
+    "axios": "^1.7.2",
     "vue-request": "^2.0.4"
+  },
+  "devDependencies": {
+    "axios-mock-adapter": "^1.22.0"
   }
 }

+ 3 - 0
packages/@vben-core/forward/request/src/index.ts

@@ -0,0 +1,3 @@
+export * from './request-client';
+export * from './use-request';
+export * from 'axios';

+ 3 - 0
packages/@vben-core/forward/request/src/request-client/index.ts

@@ -0,0 +1,3 @@
+export * from './request-client';
+export type * from './types';
+export * from './util';

+ 127 - 0
packages/@vben-core/forward/request/src/request-client/modules/canceler.test.ts

@@ -0,0 +1,127 @@
+import type { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
+
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { AxiosCanceler } from './canceler';
+
+describe('axiosCanceler', () => {
+  let axiosCanceler: AxiosCanceler;
+
+  beforeEach(() => {
+    axiosCanceler = new AxiosCanceler();
+  });
+
+  it('should generate a unique request key', () => {
+    const config: AxiosRequestConfig = {
+      data: { name: 'test' },
+      method: 'get',
+      params: { id: 1 },
+      url: '/test',
+    };
+    const requestKey = axiosCanceler.getRequestKey(config);
+    expect(requestKey).toBe('get:/test:{"id":1}:{"name":"test"}');
+  });
+
+  it('should add a request and create an AbortController', () => {
+    const config: InternalAxiosRequestConfig = {
+      data: { name: 'test' },
+      method: 'get',
+      params: { id: 1 },
+      url: '/test',
+    } as InternalAxiosRequestConfig;
+
+    const updatedConfig = axiosCanceler.addRequest(config);
+    expect(updatedConfig.signal).toBeInstanceOf(AbortSignal);
+  });
+
+  it('should cancel an existing request if a duplicate is added', () => {
+    const config: InternalAxiosRequestConfig = {
+      data: { name: 'test' },
+      method: 'get',
+      params: { id: 1 },
+      url: '/test',
+    } as InternalAxiosRequestConfig;
+
+    axiosCanceler.addRequest(config);
+    const controller = axiosCanceler.pending.get(
+      'get:/test:{"id":1}:{"name":"test"}',
+    );
+    expect(controller).toBeDefined();
+
+    if (controller) {
+      const spy = vi.spyOn(controller, 'abort');
+
+      axiosCanceler.addRequest(config);
+      expect(spy).toHaveBeenCalled();
+    }
+  });
+
+  it('should remove a request', () => {
+    const config: AxiosRequestConfig = {
+      data: { name: 'test' },
+      method: 'get',
+      params: { id: 1 },
+      url: '/test',
+    };
+
+    axiosCanceler.addRequest(config as InternalAxiosRequestConfig);
+    axiosCanceler.removeRequest(config);
+    expect(axiosCanceler.pending.size).toBe(0);
+  });
+
+  it('should remove all pending requests', () => {
+    const config1: InternalAxiosRequestConfig = {
+      data: { name: 'test1' },
+      method: 'get',
+      params: { id: 1 },
+      url: '/test1',
+    } as InternalAxiosRequestConfig;
+
+    const config2: InternalAxiosRequestConfig = {
+      data: { name: 'test2' },
+      method: 'get',
+      params: { id: 2 },
+      url: '/test2',
+    } as InternalAxiosRequestConfig;
+
+    axiosCanceler.addRequest(config1);
+    axiosCanceler.addRequest(config2);
+
+    axiosCanceler.removeAllPending();
+    expect(axiosCanceler.pending.size).toBe(0);
+  });
+
+  it('should handle empty config gracefully', () => {
+    const config = {} as InternalAxiosRequestConfig;
+    const updatedConfig = axiosCanceler.addRequest(config);
+    expect(updatedConfig.signal).toBeInstanceOf(AbortSignal);
+  });
+
+  it('should handle undefined params and data gracefully', () => {
+    const config: InternalAxiosRequestConfig = {
+      method: 'get',
+      url: '/test',
+    } as InternalAxiosRequestConfig;
+
+    const requestKey = axiosCanceler.getRequestKey(config);
+    expect(requestKey).toBe('get:/test:{}:{}');
+  });
+
+  it('should not abort if no controller exists for the request key', () => {
+    const config: InternalAxiosRequestConfig = {
+      data: { name: 'test' },
+      method: 'get',
+      params: { id: 1 },
+      url: '/test',
+    } as InternalAxiosRequestConfig;
+
+    const requestKey = axiosCanceler.getRequestKey(config);
+    const spy = vi.spyOn(AbortController.prototype, 'abort');
+
+    axiosCanceler.addRequest(config);
+    axiosCanceler.pending.delete(requestKey);
+    axiosCanceler.addRequest(config);
+
+    expect(spy).not.toHaveBeenCalled();
+  });
+});

+ 52 - 0
packages/@vben-core/forward/request/src/request-client/modules/canceler.ts

@@ -0,0 +1,52 @@
+import type {
+  AxiosRequestConfig,
+  AxiosResponse,
+  InternalAxiosRequestConfig,
+} from 'axios';
+
+class AxiosCanceler {
+  public pending: Map<string, AbortController> = new Map();
+
+  // 添加请求
+  public addRequest(
+    config: InternalAxiosRequestConfig,
+  ): InternalAxiosRequestConfig {
+    const requestKey = this.getRequestKey(config);
+    if (this.pending.has(requestKey)) {
+      // 如果存在相同的请求,取消前一个请求
+      const controller = this.pending.get(requestKey);
+      controller?.abort();
+    }
+
+    // 创建新的AbortController并添加到pending中
+    const controller = new AbortController();
+    config.signal = controller.signal;
+    this.pending.set(requestKey, controller);
+
+    return config;
+  }
+
+  // 生成请求的唯一标识
+  public getRequestKey(config: AxiosRequestConfig): string {
+    const { data = {}, method, params = {}, url } = config;
+    return `${method}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`;
+  }
+
+  /**
+   * 清除所有等待中的请求
+   */
+  public removeAllPending(): void {
+    for (const [, abortController] of this.pending) {
+      abortController?.abort();
+    }
+    this.pending.clear();
+  }
+
+  // 移除请求
+  public removeRequest(config: AxiosRequestConfig | AxiosResponse): void {
+    const requestKey = this.getRequestKey(config);
+    this.pending.delete(requestKey);
+  }
+}
+
+export { AxiosCanceler };

+ 84 - 0
packages/@vben-core/forward/request/src/request-client/modules/downloader.test.ts

@@ -0,0 +1,84 @@
+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',
+    });
+  });
+
+  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',
+    });
+  });
+
+  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',
+    );
+  });
+});

+ 30 - 0
packages/@vben-core/forward/request/src/request-client/modules/downloader.ts

@@ -0,0 +1,30 @@
+import type { AxiosRequestConfig, AxiosResponse } from 'axios';
+
+import type { RequestClient } from '../request-client';
+
+class FileDownloader {
+  private client: RequestClient;
+
+  constructor(client: RequestClient) {
+    this.client = client;
+  }
+
+  public async download(
+    url: string,
+    config?: AxiosRequestConfig,
+  ): Promise<AxiosResponse<Blob>> {
+    const finalConfig: AxiosRequestConfig = {
+      ...config,
+      responseType: 'blob',
+    };
+
+    const response = await this.client.get<AxiosResponse<Blob>>(
+      url,
+      finalConfig,
+    );
+
+    return response;
+  }
+}
+
+export { FileDownloader };

+ 33 - 0
packages/@vben-core/forward/request/src/request-client/modules/interceptor.ts

@@ -0,0 +1,33 @@
+import {
+  AxiosInstance,
+  AxiosResponse,
+  type InternalAxiosRequestConfig,
+} from 'axios';
+
+class InterceptorManager {
+  private axiosInstance: AxiosInstance;
+
+  constructor(instance: AxiosInstance) {
+    this.axiosInstance = instance;
+  }
+
+  addRequestInterceptor(
+    fulfilled: (
+      config: InternalAxiosRequestConfig,
+    ) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>,
+    rejected?: (error: any) => any,
+  ) {
+    this.axiosInstance.interceptors.request.use(fulfilled, rejected);
+  }
+
+  addResponseInterceptor(
+    fulfilled: (
+      response: AxiosResponse,
+    ) => AxiosResponse | Promise<AxiosResponse>,
+    rejected?: (error: any) => any,
+  ) {
+    this.axiosInstance.interceptors.response.use(fulfilled, rejected);
+  }
+}
+
+export { InterceptorManager };

+ 118 - 0
packages/@vben-core/forward/request/src/request-client/modules/uploader.test.ts

@@ -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',
+    );
+  });
+});

+ 32 - 0
packages/@vben-core/forward/request/src/request-client/modules/uploader.ts

@@ -0,0 +1,32 @@
+import type { AxiosRequestConfig, AxiosResponse } from 'axios';
+
+import type { RequestClient } from '../request-client';
+
+class FileUploader {
+  private client: RequestClient;
+
+  constructor(client: RequestClient) {
+    this.client = client;
+  }
+
+  public async upload(
+    url: string,
+    file: Blob | File,
+    config?: AxiosRequestConfig,
+  ): Promise<AxiosResponse> {
+    const formData = new FormData();
+    formData.append('file', file);
+
+    const finalConfig: AxiosRequestConfig = {
+      ...config,
+      headers: {
+        'Content-Type': 'multipart/form-data',
+        ...config?.headers,
+      },
+    };
+
+    return this.client.post(url, formData, finalConfig);
+  }
+}
+
+export { FileUploader };

+ 97 - 0
packages/@vben-core/forward/request/src/request-client/request-client.test.ts

@@ -0,0 +1,97 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+
+import { RequestClient } from './request-client';
+
+describe('requestClient', () => {
+  let mock: MockAdapter;
+  let requestClient: RequestClient;
+
+  beforeEach(() => {
+    mock = new MockAdapter(axios);
+    requestClient = new RequestClient();
+  });
+
+  afterEach(() => {
+    mock.reset();
+  });
+
+  it('should successfully make a GET request', async () => {
+    mock.onGet('test/url').reply(200, { data: 'response' });
+
+    const response = await requestClient.get('test/url');
+
+    expect(response.data).toEqual({ data: 'response' });
+  });
+
+  it('should successfully make a POST request', async () => {
+    const postData = { key: 'value' };
+    const mockData = { data: 'response' };
+    mock.onPost('/test/post', postData).reply(200, mockData);
+    const response = await requestClient.post('/test/post', postData);
+    expect(response.data).toEqual(mockData);
+  });
+
+  it('should successfully make a PUT request', async () => {
+    const putData = { key: 'updatedValue' };
+    const mockData = { data: 'updated response' };
+    mock.onPut('/test/put', putData).reply(200, mockData);
+    const response = await requestClient.put('/test/put', putData);
+    expect(response.data).toEqual(mockData);
+  });
+
+  it('should successfully make a DELETE request', async () => {
+    const mockData = { data: 'delete response' };
+    mock.onDelete('/test/delete').reply(200, mockData);
+    const response = await requestClient.delete('/test/delete');
+    expect(response.data).toEqual(mockData);
+  });
+
+  it('should handle network errors', async () => {
+    mock.onGet('/test/error').networkError();
+    try {
+      await requestClient.get('/test/error');
+      expect(true).toBe(false);
+    } catch (error: any) {
+      expect(error.isAxiosError).toBe(true);
+      expect(error.message).toBe('Network Error');
+    }
+  });
+
+  it('should handle timeout', async () => {
+    mock.onGet('/test/timeout').timeout();
+    try {
+      await requestClient.get('/test/timeout');
+      expect(true).toBe(false);
+    } catch (error: any) {
+      expect(error.isAxiosError).toBe(true);
+      expect(error.code).toBe('ECONNABORTED');
+    }
+  });
+
+  it('should successfully upload a file', async () => {
+    const fileData = new Blob(['file contents'], { type: 'text/plain' });
+
+    mock.onPost('/test/upload').reply((config) => {
+      return config.data instanceof FormData && config.data.has('file')
+        ? [200, { data: 'file uploaded' }]
+        : [400, { error: 'Bad Request' }];
+    });
+
+    const response = await requestClient.upload('/test/upload', fileData);
+    expect(response.data).toEqual({ data: 'file uploaded' });
+  });
+
+  it('should successfully download a file as a blob', async () => {
+    const mockFileContent = new Blob(['mock file content'], {
+      type: 'text/plain',
+    });
+
+    mock.onGet('/test/download').reply(200, mockFileContent);
+
+    const res = await requestClient.download('/test/download');
+
+    expect(res.data).toBeInstanceOf(Blob);
+  });
+});

+ 179 - 0
packages/@vben-core/forward/request/src/request-client/request-client.ts

@@ -0,0 +1,179 @@
+import type {
+  AxiosInstance,
+  AxiosRequestConfig,
+  AxiosResponse,
+  CreateAxiosDefaults,
+  InternalAxiosRequestConfig,
+} from 'axios';
+
+import { merge } from '@vben-core/toolkit';
+
+import axios from 'axios';
+
+import { AxiosCanceler } from './modules/canceler';
+import { FileDownloader } from './modules/downloader';
+import { InterceptorManager } from './modules/interceptor';
+import { FileUploader } from './modules/uploader';
+
+import type { MakeAuthorizationFn, RequestClientOptions } from './types';
+
+class RequestClient {
+  private instance: AxiosInstance;
+  private makeAuthorization: MakeAuthorizationFn | undefined;
+  public addRequestInterceptor: InterceptorManager['addRequestInterceptor'];
+  public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
+  public download: FileDownloader['download'];
+  public upload: FileUploader['upload'];
+
+  /**
+   * 构造函数,用于创建Axios实例
+   * @param {AxiosRequestConfig} config - Axios请求配置,可选
+   */
+  constructor(options: RequestClientOptions = {}) {
+    // 合并默认配置和传入的配置
+    const defaultConfig: CreateAxiosDefaults = {
+      headers: {
+        'Content-Type': 'application/json;charset=utf-8',
+      },
+      // 默认超时时间
+      timeout: 10_000,
+      withCredentials: true,
+    };
+    const { makeAuthorization, ...axiosConfig } = options;
+    const requestConfig = merge(axiosConfig, defaultConfig);
+
+    this.instance = axios.create(requestConfig);
+    this.makeAuthorization = makeAuthorization;
+
+    // 实例化拦截器管理器
+    const interceptorManager = new InterceptorManager(this.instance);
+    this.addRequestInterceptor =
+      interceptorManager.addRequestInterceptor.bind(interceptorManager);
+    this.addResponseInterceptor =
+      interceptorManager.addResponseInterceptor.bind(interceptorManager);
+
+    // 实例化文件上传器
+    const fileUploader = new FileUploader(this);
+    this.upload = fileUploader.upload.bind(fileUploader);
+    // 实例化文件下载器
+    const fileDownloader = new FileDownloader(this);
+    this.download = fileDownloader.download.bind(fileDownloader);
+
+    // 设置默认的拦截器
+    this.setupInterceptors();
+  }
+
+  private errorHandler(error: any) {
+    return Promise.reject(error);
+  }
+
+  private setupAuthorizationInterceptor() {
+    this.addRequestInterceptor((config: InternalAxiosRequestConfig) => {
+      const authorization = this.makeAuthorization?.(config);
+      if (authorization) {
+        config.headers[authorization.key || 'Authorization'] =
+          authorization.handle?.();
+      }
+      return config;
+    }, this.errorHandler);
+  }
+
+  private setupInterceptors() {
+    // 默认拦截器
+    this.setupAuthorizationInterceptor();
+    // 设置取消请求的拦截器
+    this.setupCancelerInterceptor();
+  }
+
+  /**
+   * DELETE请求方法
+   * @param {string} url - 请求的URL
+   * @param {AxiosRequestConfig} config - 请求配置(可选)
+   * @returns 返回Promise
+   */
+  public delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
+    return this.request<T>(url, { ...config, method: 'DELETE' });
+  }
+
+  /**
+   * GET请求方法
+   * @param {string} url - 请求URL
+   * @param {AxiosRequestConfig} config - 请求配置,可选
+   * @returns {Promise<AxiosResponse<T>>} 返回Axios响应Promise
+   */
+  public get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
+    return this.request<T>(url, { ...config, method: 'GET' });
+  }
+
+  /**
+   * POST请求方法
+   * @param {string} url - 请求URL
+   * @param {any} data - 请求体数据
+   * @param {AxiosRequestConfig} config - 请求配置,可选
+   * @returns {Promise<AxiosResponse<T>>} 返回Axios响应Promise
+   */
+  public post<T = any>(
+    url: string,
+    data?: any,
+    config?: AxiosRequestConfig,
+  ): Promise<T> {
+    return this.request<T>(url, { ...config, data, method: 'POST' });
+  }
+
+  /**
+   * PUT请求方法
+   * @param {string} url - 请求的URL
+   * @param {any} data - 请求体数据
+   * @param {AxiosRequestConfig} config - 请求配置(可选)
+   * @returns 返回Promise
+   */
+  public put<T = any>(
+    url: string,
+    data?: any,
+    config?: AxiosRequestConfig,
+  ): Promise<T> {
+    return this.request<T>(url, { ...config, data, method: 'PUT' });
+  }
+
+  /**
+   * 通用的请求方法
+   * @param {string} url - 请求的URL
+   * @param {AxiosRequestConfig} config - 请求配置对象
+   * @returns {Promise<AxiosResponse<T>>} 返回Axios响应Promise
+   */
+  public async request<T>(url: string, config: AxiosRequestConfig): Promise<T> {
+    try {
+      const response: AxiosResponse<T> = await this.instance({
+        url,
+        ...config,
+      });
+      return response as T;
+    } catch (error: any) {
+      throw error.response ? error.response.data : error;
+    }
+  }
+
+  public setupCancelerInterceptor() {
+    const axiosCanceler = new AxiosCanceler();
+    // 注册取消重复请求的请求拦截器
+    this.addRequestInterceptor((config: InternalAxiosRequestConfig) => {
+      return axiosCanceler.addRequest(config);
+    }, this.errorHandler);
+
+    // 注册移除请求的响应拦截器
+    this.addResponseInterceptor(
+      (response: AxiosResponse) => {
+        axiosCanceler.removeRequest(response);
+        return response;
+      },
+      (error) => {
+        if (error.config) {
+          axiosCanceler.removeRequest(error.config);
+        }
+        return Promise.reject(error);
+      },
+    );
+  }
+}
+
+export { RequestClient };

+ 24 - 0
packages/@vben-core/forward/request/src/request-client/types.ts

@@ -0,0 +1,24 @@
+import type { CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
+
+type RequestContentType =
+  | 'application/json;charset=utf-8'
+  | 'application/octet-stream;charset=utf-8'
+  | 'application/x-www-form-urlencoded;charset=utf-8'
+  | 'multipart/form-data;charset=utf-8';
+
+interface MakeAuthorization {
+  handle: () => null | string;
+  key?: string;
+}
+
+type MakeAuthorizationFn = (
+  config?: InternalAxiosRequestConfig,
+) => MakeAuthorization;
+
+interface RequestClientOptions extends CreateAxiosDefaults {
+  /**
+   * 用于生成Authorization
+   */
+  makeAuthorization?: MakeAuthorizationFn;
+}
+export type { MakeAuthorizationFn, RequestClientOptions, RequestContentType };

+ 25 - 0
packages/@vben-core/forward/request/src/request-client/util.test.ts

@@ -0,0 +1,25 @@
+import axios from 'axios';
+import { describe, expect, it } from 'vitest';
+
+import { isCancelError } from './util';
+
+describe('isCancelError', () => {
+  const source = axios.CancelToken.source();
+  source.cancel('Operation canceled by the user.');
+
+  it('should detect cancellation', () => {
+    const error = new axios.Cancel('Operation canceled by the user.');
+
+    const result = isCancelError(error);
+
+    expect(result).toBe(true);
+  });
+
+  it('should not detect cancellation on regular errors', () => {
+    const error = new Error('Regular error');
+
+    const result = isCancelError(error);
+
+    expect(result).toBe(false);
+  });
+});

+ 7 - 0
packages/@vben-core/forward/request/src/request-client/util.ts

@@ -0,0 +1,7 @@
+import axios from 'axios';
+
+function isCancelError(error: any) {
+  return axios.isCancel(error);
+}
+
+export { isCancelError };

+ 0 - 0
packages/request/src/use-request.ts → packages/@vben-core/forward/request/src/use-request.ts


+ 0 - 0
packages/request/tsconfig.json → packages/@vben-core/forward/request/tsconfig.json


+ 0 - 1
packages/request/src/index.ts

@@ -1 +0,0 @@
-export * from './use-request';

+ 51 - 0
packages/styles/src/common/entry.scss

@@ -0,0 +1,51 @@
+$max-child: 5;
+
+@for $i from 1 through $max-child {
+  * > .enter-x:nth-child(#{$i}) {
+    transform: translateX(50px);
+  }
+
+  * > .-enter-x:nth-child(#{$i}) {
+    transform: translateX(-50px);
+  }
+
+  * > .enter-x:nth-child(#{$i}),
+  * > .-enter-x:nth-child(#{$i}) {
+    // z-index: 10 - $i;
+    opacity: 0;
+    animation: enter-x-animation 0.3s ease-in-out 0.2s;
+    animation-delay: 0.1s * $i;
+    animation-fill-mode: forwards;
+  }
+
+  * > .enter-y:nth-child(#{$i}) {
+    transform: translateY(50px);
+  }
+
+  * > .-enter-y:nth-child(#{$i}) {
+    transform: translateY(-50px);
+  }
+
+  * > .enter-y:nth-child(#{$i}),
+  * > .-enter-y:nth-child(#{$i}) {
+    // z-index: 10 - $i;
+    opacity: 0;
+    animation: enter-y-animation 0.3s ease-in-out 0.2s;
+    animation-delay: 0.1s * $i;
+    animation-fill-mode: forwards;
+  }
+}
+
+@keyframes enter-x-animation {
+  to {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+@keyframes enter-y-animation {
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}

+ 133 - 99
pnpm-lock.yaml

@@ -23,8 +23,8 @@ importers:
         specifier: ^21.1.7
         version: 21.1.7
       '@types/node':
-        specifier: ^20.13.0
-        version: 20.13.0
+        specifier: ^20.14.0
+        version: 20.14.0
       '@vben/commitlint-config':
         specifier: workspace:*
         version: link:internal/lint-configs/commitlint-config
@@ -84,10 +84,10 @@ importers:
         version: 2.0.0(sass@1.77.4)(typescript@5.4.5)(vue-tsc@2.0.19(typescript@5.4.5))
       vite:
         specifier: 6.0.0-alpha.17
-        version: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+        version: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
       vitest:
         specifier: ^2.0.0-beta.3
-        version: 2.0.0-beta.3(@types/node@20.13.0)(jsdom@24.1.0)(sass@1.77.4)(terser@5.31.0)
+        version: 2.0.0-beta.3(@types/node@20.14.0)(jsdom@24.1.0)(sass@1.77.4)(terser@5.31.0)
       vue-tsc:
         specifier: ^2.0.19
         version: 2.0.19(typescript@5.4.5)
@@ -100,6 +100,9 @@ importers:
       '@vben-core/preferences':
         specifier: workspace:*
         version: link:../../packages/@vben-core/forward/preferences
+      '@vben-core/request':
+        specifier: workspace:*
+        version: link:../../packages/@vben-core/forward/request
       '@vben-core/stores':
         specifier: workspace:*
         version: link:../../packages/@vben-core/forward/stores
@@ -121,9 +124,6 @@ importers:
       '@vben/locales':
         specifier: workspace:*
         version: link:../../packages/locales
-      '@vben/request':
-        specifier: workspace:*
-        version: link:../../packages/request
       '@vben/styles':
         specifier: workspace:*
         version: link:../../packages/styles
@@ -157,13 +157,13 @@ importers:
     devDependencies:
       vite-plugin-mock:
         specifier: ^3.0.2
-        version: 3.0.2(esbuild@0.20.2)(mockjs@1.1.0)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))
+        version: 3.0.2(esbuild@0.20.2)(mockjs@1.1.0)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))
 
   internal/lint-configs/commitlint-config:
     dependencies:
       '@commitlint/cli':
         specifier: ^19.3.0
-        version: 19.3.0(@types/node@20.13.0)(typescript@5.4.5)
+        version: 19.3.0(@types/node@20.14.0)(typescript@5.4.5)
       '@commitlint/config-conventional':
         specifier: ^19.2.2
         version: 19.2.2
@@ -236,7 +236,7 @@ importers:
         version: 4.0.0(@typescript-eslint/eslint-plugin@7.11.0(@typescript-eslint/parser@7.11.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)
       eslint-plugin-vitest:
         specifier: ^0.5.4
-        version: 0.5.4(@typescript-eslint/eslint-plugin@7.11.0(@typescript-eslint/parser@7.11.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.0.0-beta.3(@types/node@20.13.0)(jsdom@24.1.0)(sass@1.77.4)(terser@5.31.0))
+        version: 0.5.4(@typescript-eslint/eslint-plugin@7.11.0(@typescript-eslint/parser@7.11.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.0.0-beta.3(@types/node@20.14.0)(jsdom@24.1.0)(sass@1.77.4)(terser@5.31.0))
       eslint-plugin-vue:
         specifier: ^9.26.0
         version: 9.26.0(eslint@8.57.0)
@@ -394,7 +394,7 @@ importers:
         version: link:../../packages/types
       vite:
         specifier: 6.0.0-alpha.17
-        version: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+        version: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
 
   internal/vite-config:
     dependencies:
@@ -415,10 +415,10 @@ importers:
         version: 2.0.2
       vite-plugin-lib-inject-css:
         specifier: ^2.1.1
-        version: 2.1.1(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))
+        version: 2.1.1(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))
       vite-plugin-vue-devtools:
         specifier: ^7.2.1
-        version: 7.2.1(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
+        version: 7.2.1(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
     devDependencies:
       '@types/html-minifier-terser':
         specifier: ^7.0.2
@@ -428,10 +428,10 @@ importers:
         version: link:../node-utils
       '@vitejs/plugin-vue':
         specifier: ^5.0.5
-        version: 5.0.5(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
+        version: 5.0.5(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
       '@vitejs/plugin-vue-jsx':
         specifier: ^4.0.0
-        version: 4.0.0(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
+        version: 4.0.0(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
       dayjs:
         specifier: ^1.11.11
         version: 1.11.11
@@ -446,22 +446,22 @@ importers:
         version: 1.77.4
       unplugin-turbo-console:
         specifier: ^1.8.6
-        version: 1.8.6(esbuild@0.20.2)(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
+        version: 1.8.6(esbuild@0.20.2)(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
       vite:
         specifier: 6.0.0-alpha.17
-        version: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+        version: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
       vite-plugin-compression:
         specifier: ^0.5.1
-        version: 0.5.1(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))
+        version: 0.5.1(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))
       vite-plugin-dts:
         specifier: ^3.9.1
-        version: 3.9.1(@types/node@20.13.0)(rollup@4.18.0)(typescript@5.4.5)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))
+        version: 3.9.1(@types/node@20.14.0)(rollup@4.18.0)(typescript@5.4.5)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))
       vite-plugin-html:
         specifier: ^3.2.2
-        version: 3.2.2(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))
+        version: 3.2.2(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))
       vite-plugin-mock:
         specifier: ^3.0.2
-        version: 3.0.2(esbuild@0.20.2)(mockjs@1.1.0)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))
+        version: 3.0.2(esbuild@0.20.2)(mockjs@1.1.0)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))
 
   packages/@vben-core/forward/helpers:
     dependencies:
@@ -496,6 +496,22 @@ importers:
         specifier: 3.4.27
         version: 3.4.27(typescript@5.4.5)
 
+  packages/@vben-core/forward/request:
+    dependencies:
+      '@vben-core/toolkit':
+        specifier: workspace:*
+        version: link:../../shared/toolkit
+      axios:
+        specifier: ^1.7.2
+        version: 1.7.2
+      vue-request:
+        specifier: ^2.0.4
+        version: 2.0.4(vue@3.4.27(typescript@5.4.5))
+    devDependencies:
+      axios-mock-adapter:
+        specifier: ^1.22.0
+        version: 1.22.0(axios@1.7.2)
+
   packages/@vben-core/forward/stores:
     dependencies:
       '@vben-core/toolkit':
@@ -787,12 +803,6 @@ importers:
         specifier: ^9.13.1
         version: 9.13.1(vue@3.4.27(typescript@5.4.5))
 
-  packages/request:
-    dependencies:
-      vue-request:
-        specifier: ^2.0.4
-        version: 2.0.4(vue@3.4.27(typescript@5.4.5))
-
   packages/styles:
     dependencies:
       '@vben-core/design':
@@ -842,7 +852,7 @@ importers:
     devDependencies:
       vitepress:
         specifier: ^1.2.2
-        version: 1.2.2(@algolia/client-search@4.23.3)(@types/node@20.13.0)(async-validator@4.2.5)(axios@1.7.2)(nprogress@0.2.0)(postcss@8.4.38)(qrcode@1.5.3)(sass@1.77.4)(search-insights@2.14.0)(terser@5.31.0)(typescript@5.4.5)
+        version: 1.2.2(@algolia/client-search@4.23.3)(@types/node@20.14.0)(async-validator@4.2.5)(axios@1.7.2)(nprogress@0.2.0)(postcss@8.4.38)(qrcode@1.5.3)(sass@1.77.4)(search-insights@2.14.0)(terser@5.31.0)(typescript@5.4.5)
       vue:
         specifier: 3.4.27
         version: 3.4.27(typescript@5.4.5)
@@ -2420,6 +2430,9 @@ packages:
   '@types/node@20.13.0':
     resolution: {integrity: sha512-FM6AOb3khNkNIXPnHFDYaHerSv8uN22C91z098AnGccVu+Pcdhi+pNUFDi0iLmPIsVE0JBD0KVS7mzUYt4nRzQ==}
 
+  '@types/node@20.14.0':
+    resolution: {integrity: sha512-5cHBxFGJx6L4s56Bubp4fglrEpmyJypsqI6RgzMfBHWUJQGWAAi8cWcgetEbZXHYXo9C2Fa4EEds/uSyS4cxmA==}
+
   '@types/normalize-package-data@2.4.4':
     resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
 
@@ -2870,6 +2883,11 @@ packages:
     resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
     engines: {node: '>= 0.4'}
 
+  axios-mock-adapter@1.22.0:
+    resolution: {integrity: sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==}
+    peerDependencies:
+      axios: '>= 0.17.0'
+
   axios@1.7.2:
     resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==}
 
@@ -4465,6 +4483,10 @@ packages:
     resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==}
     engines: {node: '>= 0.4'}
 
+  is-buffer@2.0.5:
+    resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
+    engines: {node: '>=4'}
+
   is-builtin-module@3.2.1:
     resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
     engines: {node: '>=6'}
@@ -8017,11 +8039,11 @@ snapshots:
       human-id: 1.0.2
       prettier: 2.8.8
 
-  '@commitlint/cli@19.3.0(@types/node@20.13.0)(typescript@5.4.5)':
+  '@commitlint/cli@19.3.0(@types/node@20.14.0)(typescript@5.4.5)':
     dependencies:
       '@commitlint/format': 19.3.0
       '@commitlint/lint': 19.2.2
-      '@commitlint/load': 19.2.0(@types/node@20.13.0)(typescript@5.4.5)
+      '@commitlint/load': 19.2.0(@types/node@20.14.0)(typescript@5.4.5)
       '@commitlint/read': 19.2.1
       '@commitlint/types': 19.0.3
       execa: 8.0.1
@@ -8068,7 +8090,7 @@ snapshots:
       '@commitlint/rules': 19.0.3
       '@commitlint/types': 19.0.3
 
-  '@commitlint/load@19.2.0(@types/node@20.13.0)(typescript@5.4.5)':
+  '@commitlint/load@19.2.0(@types/node@20.14.0)(typescript@5.4.5)':
     dependencies:
       '@commitlint/config-validator': 19.0.3
       '@commitlint/execute-rule': 19.0.0
@@ -8076,7 +8098,7 @@ snapshots:
       '@commitlint/types': 19.0.3
       chalk: 5.3.0
       cosmiconfig: 9.0.0(typescript@5.4.5)
-      cosmiconfig-typescript-loader: 5.0.0(@types/node@20.13.0)(cosmiconfig@9.0.0(typescript@5.4.5))(typescript@5.4.5)
+      cosmiconfig-typescript-loader: 5.0.0(@types/node@20.14.0)(cosmiconfig@9.0.0(typescript@5.4.5))(typescript@5.4.5)
       lodash.isplainobject: 4.0.6
       lodash.merge: 4.6.2
       lodash.uniq: 4.5.0
@@ -8775,23 +8797,23 @@ snapshots:
       jju: 1.4.0
       read-yaml-file: 1.1.0
 
-  '@microsoft/api-extractor-model@7.28.13(@types/node@20.13.0)':
+  '@microsoft/api-extractor-model@7.28.13(@types/node@20.14.0)':
     dependencies:
       '@microsoft/tsdoc': 0.14.2
       '@microsoft/tsdoc-config': 0.16.2
-      '@rushstack/node-core-library': 4.0.2(@types/node@20.13.0)
+      '@rushstack/node-core-library': 4.0.2(@types/node@20.14.0)
     transitivePeerDependencies:
       - '@types/node'
 
-  '@microsoft/api-extractor@7.43.0(@types/node@20.13.0)':
+  '@microsoft/api-extractor@7.43.0(@types/node@20.14.0)':
     dependencies:
-      '@microsoft/api-extractor-model': 7.28.13(@types/node@20.13.0)
+      '@microsoft/api-extractor-model': 7.28.13(@types/node@20.14.0)
       '@microsoft/tsdoc': 0.14.2
       '@microsoft/tsdoc-config': 0.16.2
-      '@rushstack/node-core-library': 4.0.2(@types/node@20.13.0)
+      '@rushstack/node-core-library': 4.0.2(@types/node@20.14.0)
       '@rushstack/rig-package': 0.5.2
-      '@rushstack/terminal': 0.10.0(@types/node@20.13.0)
-      '@rushstack/ts-command-line': 4.19.1(@types/node@20.13.0)
+      '@rushstack/terminal': 0.10.0(@types/node@20.14.0)
+      '@rushstack/ts-command-line': 4.19.1(@types/node@20.14.0)
       lodash: 4.17.21
       minimatch: 3.0.8
       resolve: 1.22.8
@@ -8967,7 +8989,7 @@ snapshots:
   '@rollup/rollup-win32-x64-msvc@4.18.0':
     optional: true
 
-  '@rushstack/node-core-library@4.0.2(@types/node@20.13.0)':
+  '@rushstack/node-core-library@4.0.2(@types/node@20.14.0)':
     dependencies:
       fs-extra: 7.0.1
       import-lazy: 4.0.0
@@ -8976,23 +8998,23 @@ snapshots:
       semver: 7.5.4
       z-schema: 5.0.5
     optionalDependencies:
-      '@types/node': 20.13.0
+      '@types/node': 20.14.0
 
   '@rushstack/rig-package@0.5.2':
     dependencies:
       resolve: 1.22.8
       strip-json-comments: 3.1.1
 
-  '@rushstack/terminal@0.10.0(@types/node@20.13.0)':
+  '@rushstack/terminal@0.10.0(@types/node@20.14.0)':
     dependencies:
-      '@rushstack/node-core-library': 4.0.2(@types/node@20.13.0)
+      '@rushstack/node-core-library': 4.0.2(@types/node@20.14.0)
       supports-color: 8.1.1
     optionalDependencies:
-      '@types/node': 20.13.0
+      '@types/node': 20.14.0
 
-  '@rushstack/ts-command-line@4.19.1(@types/node@20.13.0)':
+  '@rushstack/ts-command-line@4.19.1(@types/node@20.14.0)':
     dependencies:
-      '@rushstack/terminal': 0.10.0(@types/node@20.13.0)
+      '@rushstack/terminal': 0.10.0(@types/node@20.14.0)
       '@types/argparse': 1.0.38
       argparse: 1.0.10
       string-argv: 0.3.2
@@ -9070,7 +9092,7 @@ snapshots:
 
   '@types/conventional-commits-parser@5.0.0':
     dependencies:
-      '@types/node': 20.13.0
+      '@types/node': 20.14.0
 
   '@types/eslint@8.56.10':
     dependencies:
@@ -9082,7 +9104,7 @@ snapshots:
   '@types/fs-extra@11.0.4':
     dependencies:
       '@types/jsonfile': 6.1.4
-      '@types/node': 20.13.0
+      '@types/node': 20.14.0
 
   '@types/html-minifier-terser@7.0.2': {}
 
@@ -9090,7 +9112,7 @@ snapshots:
 
   '@types/jsdom@21.1.7':
     dependencies:
-      '@types/node': 20.13.0
+      '@types/node': 20.14.0
       '@types/tough-cookie': 4.0.5
       parse5: 7.1.2
 
@@ -9098,7 +9120,7 @@ snapshots:
 
   '@types/jsonfile@6.1.4':
     dependencies:
-      '@types/node': 20.13.0
+      '@types/node': 20.14.0
 
   '@types/linkify-it@5.0.0': {}
 
@@ -9123,6 +9145,10 @@ snapshots:
     dependencies:
       undici-types: 5.26.5
 
+  '@types/node@20.14.0':
+    dependencies:
+      undici-types: 5.26.5
+
   '@types/normalize-package-data@2.4.4': {}
 
   '@types/nprogress@0.2.3': {}
@@ -9232,24 +9258,24 @@ snapshots:
 
   '@ungap/structured-clone@1.2.0': {}
 
-  '@vitejs/plugin-vue-jsx@4.0.0(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))':
+  '@vitejs/plugin-vue-jsx@4.0.0(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))':
     dependencies:
       '@babel/core': 7.24.6
       '@babel/plugin-transform-typescript': 7.24.6(@babel/core@7.24.6)
       '@vue/babel-plugin-jsx': 1.2.2(@babel/core@7.24.6)
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
       vue: 3.4.27(typescript@5.4.5)
     transitivePeerDependencies:
       - supports-color
 
-  '@vitejs/plugin-vue@5.0.5(vite@5.2.12(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))':
+  '@vitejs/plugin-vue@5.0.5(vite@5.2.12(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))':
     dependencies:
-      vite: 5.2.12(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 5.2.12(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
       vue: 3.4.27(typescript@5.4.5)
 
-  '@vitejs/plugin-vue@5.0.5(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))':
+  '@vitejs/plugin-vue@5.0.5(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))':
     dependencies:
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
       vue: 3.4.27(typescript@5.4.5)
 
   '@vitest/expect@2.0.0-beta.3':
@@ -9374,14 +9400,14 @@ snapshots:
     transitivePeerDependencies:
       - vue
 
-  '@vue/devtools-core@7.2.1(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))':
+  '@vue/devtools-core@7.2.1(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))':
     dependencies:
       '@vue/devtools-kit': 7.2.1(vue@3.4.27(typescript@5.4.5))
       '@vue/devtools-shared': 7.2.1
       mitt: 3.0.1
       nanoid: 3.3.7
       pathe: 1.1.2
-      vite-hot-client: 0.2.3(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))
+      vite-hot-client: 0.2.3(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))
     transitivePeerDependencies:
       - vite
       - vue
@@ -9692,6 +9718,12 @@ snapshots:
     dependencies:
       possible-typed-array-names: 1.0.0
 
+  axios-mock-adapter@1.22.0(axios@1.7.2):
+    dependencies:
+      axios: 1.7.2
+      fast-deep-equal: 3.1.3
+      is-buffer: 2.0.5
+
   axios@1.7.2:
     dependencies:
       follow-redirects: 1.15.6
@@ -10079,9 +10111,9 @@ snapshots:
 
   core-js@3.37.1: {}
 
-  cosmiconfig-typescript-loader@5.0.0(@types/node@20.13.0)(cosmiconfig@9.0.0(typescript@5.4.5))(typescript@5.4.5):
+  cosmiconfig-typescript-loader@5.0.0(@types/node@20.14.0)(cosmiconfig@9.0.0(typescript@5.4.5))(typescript@5.4.5):
     dependencies:
-      '@types/node': 20.13.0
+      '@types/node': 20.14.0
       cosmiconfig: 9.0.0(typescript@5.4.5)
       jiti: 1.21.0
       typescript: 5.4.5
@@ -10828,13 +10860,13 @@ snapshots:
     optionalDependencies:
       '@typescript-eslint/eslint-plugin': 7.11.0(@typescript-eslint/parser@7.11.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
 
-  eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@7.11.0(@typescript-eslint/parser@7.11.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.0.0-beta.3(@types/node@20.13.0)(jsdom@24.1.0)(sass@1.77.4)(terser@5.31.0)):
+  eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@7.11.0(@typescript-eslint/parser@7.11.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.0.0-beta.3(@types/node@20.14.0)(jsdom@24.1.0)(sass@1.77.4)(terser@5.31.0)):
     dependencies:
       '@typescript-eslint/utils': 7.11.0(eslint@8.57.0)(typescript@5.4.5)
       eslint: 8.57.0
     optionalDependencies:
       '@typescript-eslint/eslint-plugin': 7.11.0(@typescript-eslint/parser@7.11.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
-      vitest: 2.0.0-beta.3(@types/node@20.13.0)(jsdom@24.1.0)(sass@1.77.4)(terser@5.31.0)
+      vitest: 2.0.0-beta.3(@types/node@20.14.0)(jsdom@24.1.0)(sass@1.77.4)(terser@5.31.0)
     transitivePeerDependencies:
       - supports-color
       - typescript
@@ -11542,6 +11574,8 @@ snapshots:
       call-bind: 1.0.7
       has-tostringtag: 1.0.2
 
+  is-buffer@2.0.5: {}
+
   is-builtin-module@3.2.1:
     dependencies:
       builtin-modules: 3.3.0
@@ -13989,7 +14023,7 @@ snapshots:
 
   unpipe@1.0.0: {}
 
-  unplugin-turbo-console@1.8.6(esbuild@0.20.2)(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5)):
+  unplugin-turbo-console@1.8.6(esbuild@0.20.2)(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5)):
     dependencies:
       '@rollup/pluginutils': 5.1.0(rollup@4.18.0)
       ast-kit: 0.12.2
@@ -14002,7 +14036,7 @@ snapshots:
     optionalDependencies:
       esbuild: 0.20.2
       rollup: 4.18.0
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
       vue: 3.4.27(typescript@5.4.5)
     transitivePeerDependencies:
       - uWebSockets.js
@@ -14067,17 +14101,17 @@ snapshots:
 
   validator@13.12.0: {}
 
-  vite-hot-client@0.2.3(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)):
+  vite-hot-client@0.2.3(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)):
     dependencies:
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
 
-  vite-node@2.0.0-beta.3(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0):
+  vite-node@2.0.0-beta.3(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0):
     dependencies:
       cac: 6.7.14
       debug: 4.3.5
       pathe: 1.1.2
       picocolors: 1.0.1
-      vite: 5.2.12(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 5.2.12(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -14088,18 +14122,18 @@ snapshots:
       - supports-color
       - terser
 
-  vite-plugin-compression@0.5.1(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)):
+  vite-plugin-compression@0.5.1(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)):
     dependencies:
       chalk: 4.1.2
       debug: 4.3.5
       fs-extra: 10.1.0
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
     transitivePeerDependencies:
       - supports-color
 
-  vite-plugin-dts@3.9.1(@types/node@20.13.0)(rollup@4.18.0)(typescript@5.4.5)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)):
+  vite-plugin-dts@3.9.1(@types/node@20.14.0)(rollup@4.18.0)(typescript@5.4.5)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)):
     dependencies:
-      '@microsoft/api-extractor': 7.43.0(@types/node@20.13.0)
+      '@microsoft/api-extractor': 7.43.0(@types/node@20.14.0)
       '@rollup/pluginutils': 5.1.0(rollup@4.18.0)
       '@vue/language-core': 1.8.27(typescript@5.4.5)
       debug: 4.3.5
@@ -14108,13 +14142,13 @@ snapshots:
       typescript: 5.4.5
       vue-tsc: 1.8.27(typescript@5.4.5)
     optionalDependencies:
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
     transitivePeerDependencies:
       - '@types/node'
       - rollup
       - supports-color
 
-  vite-plugin-html@3.2.2(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)):
+  vite-plugin-html@3.2.2(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)):
     dependencies:
       '@rollup/pluginutils': 4.2.1
       colorette: 2.0.20
@@ -14128,9 +14162,9 @@ snapshots:
       html-minifier-terser: 6.1.0
       node-html-parser: 5.4.2
       pathe: 0.2.0
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
 
-  vite-plugin-inspect@0.8.4(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)):
+  vite-plugin-inspect@0.8.4(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)):
     dependencies:
       '@antfu/utils': 0.7.8
       '@rollup/pluginutils': 5.1.0(rollup@4.18.0)
@@ -14141,19 +14175,19 @@ snapshots:
       perfect-debounce: 1.0.0
       picocolors: 1.0.1
       sirv: 2.0.4
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
     transitivePeerDependencies:
       - rollup
       - supports-color
 
-  vite-plugin-lib-inject-css@2.1.1(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)):
+  vite-plugin-lib-inject-css@2.1.1(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)):
     dependencies:
       '@ast-grep/napi': 0.22.4
       magic-string: 0.30.10
       picocolors: 1.0.1
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
 
-  vite-plugin-mock@3.0.2(esbuild@0.20.2)(mockjs@1.1.0)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)):
+  vite-plugin-mock@3.0.2(esbuild@0.20.2)(mockjs@1.1.0)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)):
     dependencies:
       bundle-require: 4.2.1(esbuild@0.20.2)
       chokidar: 3.6.0
@@ -14164,27 +14198,27 @@ snapshots:
       mockjs: 1.1.0
       path-to-regexp: 6.2.2
       picocolors: 1.0.1
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
     transitivePeerDependencies:
       - supports-color
 
-  vite-plugin-vue-devtools@7.2.1(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5)):
+  vite-plugin-vue-devtools@7.2.1(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5)):
     dependencies:
-      '@vue/devtools-core': 7.2.1(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
+      '@vue/devtools-core': 7.2.1(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
       '@vue/devtools-kit': 7.2.1(vue@3.4.27(typescript@5.4.5))
       '@vue/devtools-shared': 7.2.1
       execa: 8.0.1
       sirv: 2.0.4
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
-      vite-plugin-inspect: 0.8.4(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))
-      vite-plugin-vue-inspector: 5.1.2(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
+      vite-plugin-inspect: 0.8.4(rollup@4.18.0)(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))
+      vite-plugin-vue-inspector: 5.1.2(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))
     transitivePeerDependencies:
       - '@nuxt/kit'
       - rollup
       - supports-color
       - vue
 
-  vite-plugin-vue-inspector@5.1.2(vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)):
+  vite-plugin-vue-inspector@5.1.2(vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)):
     dependencies:
       '@babel/core': 7.24.6
       '@babel/plugin-proposal-decorators': 7.24.6(@babel/core@7.24.6)
@@ -14195,40 +14229,40 @@ snapshots:
       '@vue/compiler-dom': 3.4.27
       kolorist: 1.8.0
       magic-string: 0.30.10
-      vite: 6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
     transitivePeerDependencies:
       - supports-color
 
-  vite@5.2.12(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0):
+  vite@5.2.12(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0):
     dependencies:
       esbuild: 0.20.2
       postcss: 8.4.38
       rollup: 4.18.0
     optionalDependencies:
-      '@types/node': 20.13.0
+      '@types/node': 20.14.0
       fsevents: 2.3.3
       sass: 1.77.4
       terser: 5.31.0
 
-  vite@6.0.0-alpha.17(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0):
+  vite@6.0.0-alpha.17(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0):
     dependencies:
       esbuild: 0.20.2
       postcss: 8.4.38
       rollup: 4.18.0
     optionalDependencies:
-      '@types/node': 20.13.0
+      '@types/node': 20.14.0
       fsevents: 2.3.3
       sass: 1.77.4
       terser: 5.31.0
 
-  vitepress@1.2.2(@algolia/client-search@4.23.3)(@types/node@20.13.0)(async-validator@4.2.5)(axios@1.7.2)(nprogress@0.2.0)(postcss@8.4.38)(qrcode@1.5.3)(sass@1.77.4)(search-insights@2.14.0)(terser@5.31.0)(typescript@5.4.5):
+  vitepress@1.2.2(@algolia/client-search@4.23.3)(@types/node@20.14.0)(async-validator@4.2.5)(axios@1.7.2)(nprogress@0.2.0)(postcss@8.4.38)(qrcode@1.5.3)(sass@1.77.4)(search-insights@2.14.0)(terser@5.31.0)(typescript@5.4.5):
     dependencies:
       '@docsearch/css': 3.6.0
       '@docsearch/js': 3.6.0(@algolia/client-search@4.23.3)(search-insights@2.14.0)
       '@shikijs/core': 1.6.1
       '@shikijs/transformers': 1.6.1
       '@types/markdown-it': 14.1.1
-      '@vitejs/plugin-vue': 5.0.5(vite@5.2.12(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
+      '@vitejs/plugin-vue': 5.0.5(vite@5.2.12(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
       '@vue/devtools-api': 7.2.1(vue@3.4.27(typescript@5.4.5))
       '@vue/shared': 3.4.27
       '@vueuse/core': 10.10.0(vue@3.4.27(typescript@5.4.5))
@@ -14237,7 +14271,7 @@ snapshots:
       mark.js: 8.11.1
       minisearch: 6.3.0
       shiki: 1.6.1
-      vite: 5.2.12(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 5.2.12(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
       vue: 3.4.27(typescript@5.4.5)
     optionalDependencies:
       postcss: 8.4.38
@@ -14268,7 +14302,7 @@ snapshots:
       - typescript
       - universal-cookie
 
-  vitest@2.0.0-beta.3(@types/node@20.13.0)(jsdom@24.1.0)(sass@1.77.4)(terser@5.31.0):
+  vitest@2.0.0-beta.3(@types/node@20.14.0)(jsdom@24.1.0)(sass@1.77.4)(terser@5.31.0):
     dependencies:
       '@vitest/expect': 2.0.0-beta.3
       '@vitest/runner': 2.0.0-beta.3
@@ -14284,11 +14318,11 @@ snapshots:
       std-env: 3.7.0
       tinybench: 2.8.0
       tinypool: 0.9.0
-      vite: 5.2.12(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
-      vite-node: 2.0.0-beta.3(@types/node@20.13.0)(sass@1.77.4)(terser@5.31.0)
+      vite: 5.2.12(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
+      vite-node: 2.0.0-beta.3(@types/node@20.14.0)(sass@1.77.4)(terser@5.31.0)
       why-is-node-running: 2.2.2
     optionalDependencies:
-      '@types/node': 20.13.0
+      '@types/node': 20.14.0
       jsdom: 24.1.0
     transitivePeerDependencies:
       - less

+ 4 - 4
vben-admin.code-workspace

@@ -48,6 +48,10 @@
       "name": "@vben-core/preferences",
       "path": "packages/@vben-core/forward/preferences",
     },
+    {
+      "name": "@vben-core/request",
+      "path": "packages/@vben-core/forward/request",
+    },
     {
       "name": "@vben-core/stores",
       "path": "packages/@vben-core/forward/stores",
@@ -116,10 +120,6 @@
       "name": "@vben/locales",
       "path": "packages/locales",
     },
-    {
-      "name": "@vben/request",
-      "path": "packages/request",
-    },
     {
       "name": "@vben/styles",
       "path": "packages/styles",