2026-05-29 14:21:32 +08:00

155 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 };