fix(ipam): 修复IP管理页面网段筛选不生效及下拉框过窄
- 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 记录
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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
|
||||
Reference in New Issue
Block a user