1e1e5957e8
- Ips.vue: el-select 添加 width: 280px,修复网段下拉框过窄 - Ips.vue: query 参数 networkId → network_id(与 FastAPI snake_case 一致) - Scan.vue: el-select 用 network.id 代替整个 network 对象作为 value,修复切换网段不生效 - enhanced_scan_service.py: 替换失效的 scapy ARP 扫描为 arp-scan 命令;扫描结果自动创建缺失 IP 记录
118 lines
3.4 KiB
JavaScript
118 lines
3.4 KiB
JavaScript
import axios from 'axios'
|
||
import { ElMessage } from 'element-plus'
|
||
import { authStore } from '@/utils/auth'
|
||
|
||
const api = axios.create({
|
||
baseURL: '/api/v1',
|
||
timeout: 30000,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
|
||
// 请求拦截器:自动带 Authorization header
|
||
api.interceptors.request.use(
|
||
config => {
|
||
const token = authStore.getAccessToken()
|
||
if (token) {
|
||
config.headers = config.headers || {}
|
||
config.headers.Authorization = `Bearer ${token}`
|
||
}
|
||
return config
|
||
},
|
||
error => Promise.reject(error)
|
||
)
|
||
|
||
// 标记正在刷新 token,防止并发 401 触发多次刷新
|
||
let isRefreshing = false
|
||
let pendingQueue = []
|
||
|
||
function flushQueue(newToken) {
|
||
pendingQueue.forEach(cb => cb(newToken))
|
||
pendingQueue = []
|
||
}
|
||
|
||
function queueRefresh(cb) {
|
||
pendingQueue.push(cb)
|
||
}
|
||
|
||
// 响应拦截器:401 时尝试 refresh token;refresh 失败就跳登录
|
||
api.interceptors.response.use(
|
||
response => {
|
||
return response.data
|
||
},
|
||
async error => {
|
||
const originalRequest = error.config || {}
|
||
const status = error.response?.status
|
||
const detail = error.response?.data?.detail
|
||
|
||
// 401 且不是 login/refresh 本身:尝试刷新 token
|
||
if (status === 401 && !originalRequest._retry && !originalRequest.url?.includes('/auth/')) {
|
||
const refreshToken = authStore.getRefreshToken()
|
||
if (!refreshToken) {
|
||
authStore.clear()
|
||
redirectToLogin()
|
||
return Promise.reject(error)
|
||
}
|
||
|
||
if (isRefreshing) {
|
||
// 已有刷新请求在进行,等它完成后再重试
|
||
return new Promise(resolve => {
|
||
queueRefresh(newToken => {
|
||
if (newToken) {
|
||
originalRequest._retry = true
|
||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||
resolve(api(originalRequest))
|
||
} else {
|
||
redirectToLogin()
|
||
resolve(Promise.reject(error))
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
originalRequest._retry = true
|
||
isRefreshing = true
|
||
try {
|
||
const formBody = new URLSearchParams()
|
||
formBody.append('refresh_token', refreshToken)
|
||
const resp = await axios.post('/api/v1/auth/refresh', formBody, {
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||
})
|
||
const newToken = resp.data?.access_token
|
||
if (newToken) {
|
||
authStore.setAccessToken(newToken)
|
||
if (resp.data?.refresh_token) {
|
||
authStore.setRefreshToken(resp.data.refresh_token)
|
||
}
|
||
isRefreshing = false
|
||
flushQueue(newToken)
|
||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||
return api(originalRequest)
|
||
} else {
|
||
throw new Error('refresh 响应缺少 access_token')
|
||
}
|
||
} catch (refreshErr) {
|
||
isRefreshing = false
|
||
flushQueue(null)
|
||
authStore.clear()
|
||
redirectToLogin()
|
||
return Promise.reject(refreshErr)
|
||
}
|
||
}
|
||
|
||
// 非 401 或 refresh 后仍失败的:弹 toast(保持原行为,避免 view 重复弹)
|
||
console.error('API Error:', error)
|
||
ElMessage.error(typeof detail === 'string' ? detail : '请求失败')
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
function redirectToLogin() {
|
||
if (typeof window !== 'undefined' && window.location.pathname !== '/login') {
|
||
const next = encodeURIComponent(window.location.pathname + window.location.search)
|
||
window.location.href = `/login?next=${next}`
|
||
}
|
||
}
|
||
|
||
export default api |