admin_new/src/utils/request.ts
Sisyphus 4899bc1793 fix(F006-W18): admin 前端 BUG A/B/C (客户运维反馈)
BUG A: APIKey 申请搜企业搜不到
  - ApplyApiKeyDialog.vue loadCustomers 加 name 参数调后端, 不再本地 filter
  - 去掉 filteredCustomers computed, 表格直接用 customers
  - searchKey 输入加 debounce 300ms watch 调 loadCustomers

BUG B: 企业列表/行业/标签管理等菜单不显示
  - menu.ts filterByFrontendTruth 加 url 规范化匹配
  - 后端 url 带 / (如 /user/enterprise/enterprise), MENU_LABELS 不带 /
  - normalizeUrl = url.replace(/^\//,"") 后再 has() 匹配

BUG C: baseURL 空串 巨变数字 nginx 不通
  - request.ts baseURL:"" 改 "/api"
  - 巨变数字 nginx 只有 /api/ location (没独立 /sys/ /auth/ 等)
  - devbox nginx /api/ 也去前缀 (rewrite ^/api/(.*)\$ /\$1), 兼容

dist 验证: baseURL:"/api" ✓
devbox 实测: name=郑州巨变 返 1 条命中
2026-07-14 19:15:48 +08:00

114 lines
3.6 KiB
TypeScript

/**
* 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<T = any> {
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<Menu[]>('/sys/menu/nav')
* 等价于 const res = await http.get(...); res.data.data
*/
export async function httpGet<T = any>(url: string, params?: Record<string, any>): Promise<T> {
const res = await http.get<ApiEnvelope<T>>(url, { params })
return res.data.data
}
export async function httpPost<T = any>(url: string, body?: any): Promise<T> {
const res = await http.post<ApiEnvelope<T>>(url, body)
return res.data.data
}
export async function httpPut<T = any>(url: string, body?: any): Promise<T> {
const res = await http.put<ApiEnvelope<T>>(url, body)
return res.data.data
}
export async function httpDelete<T = any>(url: string, params?: Record<string, any>): Promise<T> {
const res = await http.delete<ApiEnvelope<T>>(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')
}