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 条命中
114 lines
3.6 KiB
TypeScript
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')
|
|
}
|