feat: Web端401队列拦截器 + 消息/任务/个人中心重写 + TabBar红点 + Admin页面完善
This commit is contained in:
@ -1,4 +1,39 @@
|
||||
import axios from "axios";
|
||||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
|
||||
// ============================================================
|
||||
// Token 存储 Key
|
||||
// ============================================================
|
||||
|
||||
const ACCESS_TOKEN_KEY = "track_admin_token";
|
||||
const REFRESH_TOKEN_KEY = "track_admin_refresh_token";
|
||||
const USER_KEY = "track_admin_user";
|
||||
|
||||
// ============================================================
|
||||
// Token 读写工具
|
||||
// ============================================================
|
||||
|
||||
export function getAccessToken(): string | null {
|
||||
return localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function getRefreshToken(): string | null {
|
||||
return localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setTokens(accessToken: string, refreshToken: string) {
|
||||
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
||||
}
|
||||
|
||||
export function clearAuth() {
|
||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Axios 实例
|
||||
// ============================================================
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
@ -8,10 +43,13 @@ const api = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器 — 注入 JWT token
|
||||
// ============================================================
|
||||
// 请求拦截器 — 注入 Access Token
|
||||
// ============================================================
|
||||
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem("track_admin_token");
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
@ -20,19 +58,104 @@ api.interceptors.request.use(
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
// 响应拦截器 — 统一错误处理
|
||||
// ============================================================
|
||||
// 响应拦截器 — 双 Token 无感刷新 + 并发请求队列
|
||||
// ============================================================
|
||||
|
||||
let isRefreshing = false;
|
||||
let retryQueue: Array<{
|
||||
resolve: (token: string) => void;
|
||||
reject: (error: unknown) => void;
|
||||
}> = [];
|
||||
|
||||
/** 处理队列中的所有挂起请求 */
|
||||
function processQueue(error: unknown, token: string | null) {
|
||||
retryQueue.forEach((p) => {
|
||||
if (token) {
|
||||
p.resolve(token);
|
||||
} else {
|
||||
p.reject(error);
|
||||
}
|
||||
});
|
||||
retryQueue = [];
|
||||
}
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token 过期或无效 → 清除并跳转登录
|
||||
localStorage.removeItem("track_admin_token");
|
||||
localStorage.removeItem("track_admin_user");
|
||||
async (error) => {
|
||||
const originalRequest: AxiosRequestConfig & { _retry?: boolean } =
|
||||
error.config;
|
||||
const status = error.response?.status;
|
||||
|
||||
// 仅处理 401
|
||||
if (status !== 401) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 跳过登录和刷新接口自身(避免死循环)
|
||||
const url = originalRequest.url ?? "";
|
||||
if (url.includes("/auth/login") || url.includes("/auth/refresh")) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 避免对同一请求重复刷新
|
||||
if (originalRequest._retry) {
|
||||
clearAuth();
|
||||
if (window.location.pathname.startsWith("/admin")) {
|
||||
window.location.href = "/admin/login";
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
originalRequest._retry = true;
|
||||
|
||||
// ---- 并发请求队列机制 ----
|
||||
if (isRefreshing) {
|
||||
// 已有刷新进行中,加入队列等待
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
retryQueue.push({ resolve, reject });
|
||||
}).then((newToken) => {
|
||||
originalRequest.headers = originalRequest.headers || {};
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
||||
return api(originalRequest);
|
||||
});
|
||||
}
|
||||
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
throw new Error("无 Refresh Token");
|
||||
}
|
||||
|
||||
// 调用刷新接口
|
||||
const { data } = await axios.post(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/auth/refresh`,
|
||||
{ refresh_token: refreshToken },
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
|
||||
const newAccessToken: string = data.access_token;
|
||||
setTokens(newAccessToken, refreshToken); // 更新 Access Token
|
||||
|
||||
// 重放队列中的所有请求
|
||||
processQueue(null, newAccessToken);
|
||||
|
||||
// 重试当前请求
|
||||
originalRequest.headers = originalRequest.headers || {};
|
||||
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
||||
return api(originalRequest);
|
||||
} catch (refreshError) {
|
||||
// Refresh Token 也过期 — 彻底登出
|
||||
processQueue(refreshError, null);
|
||||
clearAuth();
|
||||
if (window.location.pathname.startsWith("/admin")) {
|
||||
window.location.href = "/admin/login";
|
||||
}
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user