155 lines
4.6 KiB
TypeScript
155 lines
4.6 KiB
TypeScript
import Request from 'luch-request';
|
||
import { getCache, removeCache } from '@/utils/cache';
|
||
|
||
/**
|
||
* 学小乐 AI 通用 HTTP 客户端(基于 luch-request)
|
||
*
|
||
* - 自动注入 Bearer Token
|
||
* - 自动注入租户头 x-tenant-id
|
||
* - code !== 200 全局 toast 错误信息
|
||
* - 401 自动清缓存并跳转登录
|
||
* - 网络异常时弹窗提示
|
||
*/
|
||
|
||
const HOST = (import.meta.env.VITE_BASE_URL as string) || '';
|
||
const BASE_URL = `${HOST}/api/main`;
|
||
const TENANT_ID = '1966391196339204098';
|
||
let isReloginHandling = false;
|
||
|
||
interface BackendResp<T = any> {
|
||
code: number;
|
||
data: T;
|
||
message?: string;
|
||
msg?: string;
|
||
[key: string]: any;
|
||
}
|
||
|
||
export class HttpError extends Error {
|
||
code: number;
|
||
data: any;
|
||
constructor(message: string, code: number, data?: any) {
|
||
super(message);
|
||
this.code = code;
|
||
this.data = data;
|
||
}
|
||
}
|
||
|
||
const http = new Request({
|
||
baseURL: BASE_URL,
|
||
timeout: 30000,
|
||
header: {
|
||
'Content-Type': 'application/json;charset=UTF-8',
|
||
'x-tenant-id': TENANT_ID,
|
||
},
|
||
});
|
||
|
||
const handleUnauthorized = (message = '登录已失效,请重新登录') => {
|
||
if (isReloginHandling) return;
|
||
isReloginHandling = true;
|
||
removeCache('token');
|
||
removeCache('userInfo');
|
||
removeCache('teacherInfo');
|
||
removeCache('role');
|
||
uni.showToast({ title: message, icon: 'none', duration: 2500 });
|
||
// 避免并发 401 触发多次跳转与多次弹窗
|
||
setTimeout(() => {
|
||
uni.reLaunch({ url: '/pages/login/index' });
|
||
}, 200);
|
||
};
|
||
|
||
// ----------- 请求拦截 -----------
|
||
http.interceptors.request.use(
|
||
(config) => {
|
||
const token = getCache('token');
|
||
if (token && config.header) {
|
||
// 默认带上 Authorization;接口可显式覆盖(传空字符串以匿名调用)
|
||
if (!('Authorization' in config.header)) {
|
||
(config.header as any).Authorization = `Bearer ${token}`;
|
||
}
|
||
}
|
||
// 始终携带租户 ID
|
||
if (config.header && !('x-tenant-id' in config.header)) {
|
||
(config.header as any)['x-tenant-id'] = TENANT_ID;
|
||
}
|
||
return config;
|
||
},
|
||
(err) => Promise.reject(err),
|
||
);
|
||
|
||
// ----------- 响应拦截 -----------
|
||
http.interceptors.response.use(
|
||
(response: any) => {
|
||
const data = response.data as BackendResp;
|
||
if (!data || typeof data !== 'object') {
|
||
return response;
|
||
}
|
||
|
||
// 兼容不同后端返回规范:部分接口成功码为 0(并可能带 success=true)
|
||
if (data.code === 200 || data.code === 0) {
|
||
return data;
|
||
}
|
||
|
||
if (data.code === 401) {
|
||
const msg401 = data.message || data.msg || '登录已失效,请重新登录';
|
||
handleUnauthorized(msg401);
|
||
return Promise.reject(new HttpError(msg401, 401, data));
|
||
}
|
||
|
||
// 业务错误
|
||
const msg = data.message || data.msg || '请求失败';
|
||
if (!(response.config as any)?.silent) {
|
||
uni.showToast({ title: msg, icon: 'none', duration: 2500 });
|
||
}
|
||
return Promise.reject(new HttpError(msg, data.code, data));
|
||
},
|
||
(err: any) => {
|
||
const status = err?.statusCode || err?.status;
|
||
let msg = err?.errMsg || err?.message || '网络异常';
|
||
|
||
if (status === 401) {
|
||
msg = '登录已失效,请重新登录';
|
||
handleUnauthorized(msg);
|
||
} else if (status === 500) {
|
||
msg = '服务异常,请稍后重试';
|
||
}
|
||
|
||
if (status !== 401) {
|
||
uni.showToast({ title: msg, icon: 'none', duration: 2500 });
|
||
}
|
||
return Promise.reject(new HttpError(msg, status || -1, err));
|
||
},
|
||
);
|
||
|
||
/**
|
||
* 默认导出 luch-request 实例
|
||
* 同时暴露通用调用方法,兼容老接口的 `request({ url, method, data, params, headers })` 风格
|
||
*/
|
||
export interface RequestOptions {
|
||
url: string;
|
||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||
data?: anyObj;
|
||
params?: anyObj;
|
||
headers?: anyObj;
|
||
silent?: boolean;
|
||
}
|
||
|
||
export const request = <T = any>(opt: RequestOptions): Promise<BackendResp<T>> => {
|
||
const cfg: any = {
|
||
url: opt.url,
|
||
method: opt.method || 'GET',
|
||
custom: { silent: opt.silent },
|
||
};
|
||
if (opt.headers) cfg.header = opt.headers;
|
||
if (cfg.method === 'GET') {
|
||
cfg.params = opt.params || opt.data;
|
||
} else {
|
||
cfg.data = opt.data;
|
||
if (opt.params) cfg.params = opt.params;
|
||
}
|
||
if (opt.silent) cfg.silent = true;
|
||
return http.request(cfg) as Promise<BackendResp<T>>;
|
||
};
|
||
|
||
export default request;
|
||
export { http, HOST, BASE_URL, TENANT_ID };
|