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:
Your Name
2026-07-18 09:45:09 +08:00
commit 1e1e5957e8
103 changed files with 13196 additions and 0 deletions
+118
View File
@@ -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 tokenrefresh 失败就跳登录
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