/** * F006 统一 HTTP 客户端 * W3.7 基础:所有 store 用这个 http 实例,自动带 token,401 跳登录,错误 toast * * 用法: * import { http } from '@/utils/request' * const res = await http.get('/sys/menu/nav') * // 业务错误已 reject(用 try/catch 处理),成功自动返回 data.data */ import axios, { type AxiosInstance, type InternalAxiosRequestConfig } from 'axios' import { ElMessage } from 'element-plus' export interface ApiEnvelope { code: number msg?: string | null data: T } const STORAGE_KEY = 'access_token' export const http: AxiosInstance = axios.create({ // F002-W18 修: baseURL '/api' 让巨变数字 nginx /api/ location 转发 (去 /api 前缀) // devbox nginx 同样有 /api/ location (rewrite ^/api/(.*)$ /$1 break), 兼容 baseURL: '/api', timeout: 15000 }) http.interceptors.request.use((config: InternalAxiosRequestConfig) => { // 跳过 OAuth2 登录请求(用 Basic auth 不加 Bearer) if (config.url?.includes('/auth/oauth/token')) return config const token = localStorage.getItem(STORAGE_KEY) if (token) { config.headers = config.headers ?? {} config.headers.Authorization = `Bearer ${token}` } return config }) http.interceptors.response.use( (response) => { const body = response.data as ApiEnvelope | undefined if (body && typeof body === 'object' && 'code' in body) { if (body.code === 0) return response // 业务级 code:401 (如 /openapi/** 带 token 时 Spring Security 返未授权) // 不清 token 不跳登录,只 reject 给调用方处理 // 真正的 token 过期走 error 拦截器 (HTTP status 401) return Promise.reject(body) } return response }, (error) => { const status = error.response?.status if (status === 401) { handleAuthExpired() } else { const msg = error.response?.data?.msg || error.response?.data?.message || error.message || '请求失败' ElMessage.error(msg) } return Promise.reject(error) } ) function handleAuthExpired() { ElMessage.warning('登录已过期,请重新登录') localStorage.removeItem(STORAGE_KEY) localStorage.removeItem('username') localStorage.removeItem('realName') localStorage.removeItem('headUrl') localStorage.removeItem('superAdmin') localStorage.removeItem('permissions') // 用 location 跳,避免 router 状态被污染 const base = import.meta.env.BASE_URL || '/admin/' window.location.href = base + 'login' } /** * 简化业务调用: * const data = await httpGet('/sys/menu/nav') * 等价于 const res = await http.get(...); res.data.data */ export async function httpGet(url: string, params?: Record): Promise { const res = await http.get>(url, { params }) return res.data.data } export async function httpPost(url: string, body?: any): Promise { const res = await http.post>(url, body) return res.data.data } export async function httpPut(url: string, body?: any): Promise { const res = await http.put>(url, body) return res.data.data } export async function httpDelete(url: string, params?: Record): Promise { const res = await http.delete>(url, { params }) return res.data.data } /** 手动清 token(登出时用) */ export function clearAuth() { localStorage.removeItem(STORAGE_KEY) localStorage.removeItem('username') localStorage.removeItem('realName') localStorage.removeItem('headUrl') localStorage.removeItem('superAdmin') localStorage.removeItem('permissions') }