feat: add new interfaces, services, and utilities for API integration
refactor: reorganize type definitions and improve type safety build: add lodash and @types/lodash as dependencies chore: update tsconfig and vite config for path aliases style: improve code organization and add documentation comments fix: correct type usage in LanguageSwitcher component perf: implement snackbar provider for global notifications test: add new test interfaces and utility functions ci: update pnpm-lock.yaml with new dependencies
This commit is contained in:
24
src/utils/common.ts
Normal file
24
src/utils/common.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import isObject from 'lodash/isObject';
|
||||
import snakeCase from 'lodash/snakeCase';
|
||||
|
||||
export const isFormData = (data: unknown): data is FormData => {
|
||||
return data instanceof FormData;
|
||||
};
|
||||
|
||||
const excludedFields = ['img2txt_id', 'mcpServers'];
|
||||
|
||||
const isExcludedField = (key: string) => {
|
||||
return excludedFields.includes(key);
|
||||
};
|
||||
|
||||
export const convertTheKeysOfTheObjectToSnake = (data: unknown) => {
|
||||
if (isObject(data) && !isFormData(data)) {
|
||||
return Object.keys(data).reduce<Record<string, any>>((pre, cur) => {
|
||||
const value = (data as Record<string, any>)[cur];
|
||||
pre[isFormData(value) || isExcludedField(cur) ? cur : snakeCase(cur)] =
|
||||
value;
|
||||
return pre;
|
||||
}, {});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
188
src/utils/request.ts
Normal file
188
src/utils/request.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import type { ResponseType } from '@/interfaces/database/base';
|
||||
import i18n from '@/locales';
|
||||
import axios from 'axios';
|
||||
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
import { snackbar, notification } from '@/utils/snackbarInstance';
|
||||
|
||||
const FAILED_TO_FETCH = 'Failed to fetch';
|
||||
|
||||
export const RetcodeMessage = {
|
||||
200: i18n.t('message.200'),
|
||||
201: i18n.t('message.201'),
|
||||
202: i18n.t('message.202'),
|
||||
204: i18n.t('message.204'),
|
||||
400: i18n.t('message.400'),
|
||||
401: i18n.t('message.401'),
|
||||
403: i18n.t('message.403'),
|
||||
404: i18n.t('message.404'),
|
||||
406: i18n.t('message.406'),
|
||||
410: i18n.t('message.410'),
|
||||
413: i18n.t('message.413'),
|
||||
422: i18n.t('message.422'),
|
||||
500: i18n.t('message.500'),
|
||||
502: i18n.t('message.502'),
|
||||
503: i18n.t('message.503'),
|
||||
504: i18n.t('message.504'),
|
||||
};
|
||||
|
||||
export type ResultCode =
|
||||
| 200
|
||||
| 201
|
||||
| 202
|
||||
| 204
|
||||
| 400
|
||||
| 401
|
||||
| 403
|
||||
| 404
|
||||
| 406
|
||||
| 410
|
||||
| 413
|
||||
| 422
|
||||
| 500
|
||||
| 502
|
||||
| 503
|
||||
| 504;
|
||||
|
||||
// 获取授权token
|
||||
const getAuthorization = (): string => {
|
||||
return localStorage.getItem('token') || '';
|
||||
};
|
||||
|
||||
// 重定向到登录页
|
||||
const redirectToLogin = (): void => {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
};
|
||||
|
||||
// 转换对象键为snake_case
|
||||
const convertTheKeysOfTheObjectToSnake = (obj: any): any => {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(convertTheKeysOfTheObjectToSnake);
|
||||
}
|
||||
|
||||
const result: any = {};
|
||||
Object.keys(obj).forEach(key => {
|
||||
const snakeKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
||||
result[snakeKey] = convertTheKeysOfTheObjectToSnake(obj[key]);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 创建axios实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 300000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// 转换数据格式
|
||||
if (config.data) {
|
||||
config.data = convertTheKeysOfTheObjectToSnake(config.data);
|
||||
}
|
||||
if (config.params) {
|
||||
config.params = convertTheKeysOfTheObjectToSnake(config.params);
|
||||
}
|
||||
|
||||
// 添加授权头
|
||||
const token = getAuthorization();
|
||||
if (token && !config.headers?.skipToken) {
|
||||
config.headers[Authorization] = token;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { status } = response;
|
||||
|
||||
// 处理特定状态码
|
||||
if (status === 413 || status === 504) {
|
||||
snackbar.error(RetcodeMessage[status as ResultCode]);
|
||||
}
|
||||
|
||||
// 处理blob类型响应
|
||||
if (response.config.responseType === 'blob') {
|
||||
return response;
|
||||
}
|
||||
|
||||
const data: ResponseType = response.data;
|
||||
|
||||
// 处理业务错误码
|
||||
if (data?.code === 100) {
|
||||
snackbar.error(data?.message);
|
||||
} else if (data?.code === 401) {
|
||||
// notification.error({
|
||||
// message: data?.message,
|
||||
// description: data?.message,
|
||||
// duration: 3,
|
||||
// });
|
||||
notification.error(data?.message);
|
||||
redirectToLogin();
|
||||
} else if (data?.code !== 0) {
|
||||
// notification.error({
|
||||
// message: `${i18n.t('message.hint')} : ${data?.code}`,
|
||||
// description: data?.message,
|
||||
// duration: 3,
|
||||
// });
|
||||
notification.error(`${i18n.t('message.hint')} : ${data?.code}`, data?.message);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
// 处理网络错误
|
||||
if (error.message === FAILED_TO_FETCH || !error.response) {
|
||||
// notification.error({
|
||||
// description: i18n.t('message.networkAnomalyDescription'),
|
||||
// message: i18n.t('message.networkAnomaly'),
|
||||
// });
|
||||
notification.error(i18n.t('message.networkAnomaly'), i18n.t('message.networkAnomalyDescription'));
|
||||
} else if (error.response) {
|
||||
const { status, statusText } = error.response;
|
||||
const errorText = RetcodeMessage[status as ResultCode] || statusText;
|
||||
|
||||
// notification.error({
|
||||
// message: `${i18n.t('message.requestError')} ${status}`,
|
||||
// description: errorText,
|
||||
// });
|
||||
notification.error(`${i18n.t('message.requestError')} ${status}`, errorText);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default request;
|
||||
|
||||
// 便捷方法
|
||||
export const get = (url: string, config?: AxiosRequestConfig) => {
|
||||
return request.get(url, config);
|
||||
};
|
||||
|
||||
export const post = (url: string, data?: any, config?: AxiosRequestConfig) => {
|
||||
return request.post(url, data, config);
|
||||
};
|
||||
|
||||
export const put = (url: string, data?: any, config?: AxiosRequestConfig) => {
|
||||
return request.put(url, data, config);
|
||||
};
|
||||
|
||||
export const del = (url: string, config?: AxiosRequestConfig) => {
|
||||
return request.delete(url, config);
|
||||
};
|
||||
29
src/utils/snackbarInstance.ts
Normal file
29
src/utils/snackbarInstance.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// 为非组件文件提供全局 snackbar 实例
|
||||
const getSnackbarInstance = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return (window as any).__snackbarInstance;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const snackbar = {
|
||||
success: (msg: string, duration?: number) =>
|
||||
getSnackbarInstance()?.showMessage.success(msg, duration),
|
||||
error: (msg: string, duration?: number) =>
|
||||
getSnackbarInstance()?.showMessage.error(msg, duration),
|
||||
warning: (msg: string, duration?: number) =>
|
||||
getSnackbarInstance()?.showMessage.warning(msg, duration),
|
||||
info: (msg: string, duration?: number) =>
|
||||
getSnackbarInstance()?.showMessage.info(msg, duration),
|
||||
};
|
||||
|
||||
export const notification = {
|
||||
success: (title: string, message?: string, duration?: number) =>
|
||||
getSnackbarInstance()?.showNotification.success(title, message, duration),
|
||||
error: (title: string, message?: string, duration?: number) =>
|
||||
getSnackbarInstance()?.showNotification.error(title, message, duration),
|
||||
warning: (title: string, message?: string, duration?: number) =>
|
||||
getSnackbarInstance()?.showNotification.warning(title, message, duration),
|
||||
info: (title: string, message?: string, duration?: number) =>
|
||||
getSnackbarInstance()?.showNotification.info(title, message, duration),
|
||||
};
|
||||
Reference in New Issue
Block a user