140 lines
5.1 KiB
JavaScript
140 lines
5.1 KiB
JavaScript
import axios from 'axios'
|
||
import { ElMessage } from 'element-plus'
|
||
import router from '../router'
|
||
import { hasChinese, responseErrorText } from '../utils/errorText'
|
||
import {
|
||
captureOrganizationContext,
|
||
currentReadSignal,
|
||
getOrganizationId,
|
||
isCurrentOrganizationContext,
|
||
OrganizationContextSupersededError
|
||
} from '../utils/organizationContext'
|
||
|
||
const request = axios.create({
|
||
baseURL: '/api',
|
||
timeout: 30000,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
|
||
// 不需要公司上下文的接口白名单:认证、用户/组织/角色管理。
|
||
// 其余业务接口一律带 X-Organization-ID;websocket-ticket 属于组织级,不在白名单内。
|
||
const ORGANIZATION_EXEMPT_PATHS = [
|
||
'/auth/login',
|
||
'/auth/logout',
|
||
'/auth/me',
|
||
'/auth/organizations',
|
||
'/auth/organization-context',
|
||
'/organizations',
|
||
'/roles',
|
||
'/permissions',
|
||
'/users'
|
||
]
|
||
|
||
function isOrganizationExempt(url) {
|
||
const path = (url || '').split('?')[0]
|
||
return ORGANIZATION_EXEMPT_PATHS.some(exempt => path === exempt || path.startsWith(`${exempt}/`))
|
||
}
|
||
|
||
// 单次请求可通过 config.silent 关闭全局错误提示。取票等后台辅助请求失败时,
|
||
// 不应让用户看到与自己操作无关的报错;默认不静默(未显式置 true 即照旧提示)。
|
||
// 注意:silent 只抑制“业务/连接类”提示,不影响 401 的登录态全局处理。
|
||
function isSilent(config) {
|
||
return config?.silent === true
|
||
}
|
||
|
||
// Request interceptor - add auth token
|
||
request.interceptors.request.use(
|
||
(config) => {
|
||
const token = localStorage.getItem('wmms_token')
|
||
if (token) {
|
||
config.headers.Authorization = `Bearer ${token}`
|
||
}
|
||
if (!isOrganizationExempt(config.url)) {
|
||
const organizationId = getOrganizationId()
|
||
if (organizationId) {
|
||
config.headers['X-Organization-ID'] = String(organizationId)
|
||
}
|
||
// 记下发起时的上下文快照,响应回来时用它判断是否已被切换作废。
|
||
config.__organizationContext = captureOrganizationContext()
|
||
// 读请求挂上共享信号:切换公司时统一 abort,尚未返回的旧读请求被取消。
|
||
const method = (config.method || 'get').toLowerCase()
|
||
if (!config.signal && method === 'get') {
|
||
config.signal = currentReadSignal()
|
||
}
|
||
}
|
||
console.log('API请求:', config.method?.toUpperCase(), config.url)
|
||
return config
|
||
},
|
||
(error) => Promise.reject(error)
|
||
)
|
||
|
||
// Response interceptor - handle errors and 401
|
||
request.interceptors.response.use(
|
||
(response) => {
|
||
// 响应属于已被切换掉的旧公司:丢弃且不提示,页面只应关心新公司的数据。
|
||
const context = response.config?.__organizationContext
|
||
if (context && !isCurrentOrganizationContext(context)) {
|
||
return Promise.reject(new OrganizationContextSupersededError())
|
||
}
|
||
const res = response.data
|
||
if (res.code === 0) {
|
||
return res
|
||
}
|
||
if (!isSilent(response.config)) {
|
||
ElMessage.error(hasChinese(res.message) ? res.message : '请求失败')
|
||
}
|
||
return Promise.reject(new Error(res.message))
|
||
},
|
||
(error) => {
|
||
// 主动取消(切换公司)不是失败,静默丢弃,不要弹提示。
|
||
if (axios.isCancel(error)) {
|
||
return Promise.reject(error)
|
||
}
|
||
// 旧公司的延迟错误同样不得影响新页面,也不能弹提示。
|
||
const context = error.config?.__organizationContext
|
||
if (context && !isCurrentOrganizationContext(context)) {
|
||
return Promise.reject(new OrganizationContextSupersededError())
|
||
}
|
||
const silent = isSilent(error.config)
|
||
if (error.response?.status === 401) {
|
||
// 登录态失效的全局处理保持原样:清令牌并跳登录页。这与请求是否为 silent 无关,
|
||
// silent 只抑制“业务/连接类”错误提示。
|
||
localStorage.removeItem('wmms_token')
|
||
localStorage.removeItem('wmms_user')
|
||
// 登录页自己会把失败原因写在表单里,再弹一句"登录已过期"只会让人困惑
|
||
if (router.currentRoute.value.path !== '/login') {
|
||
router.push('/login')
|
||
ElMessage.error('登录已过期,请重新登录')
|
||
}
|
||
} else if (error.response?.status === 422) {
|
||
// Validation errors - parse and show friendly message
|
||
const data = error.response?.data
|
||
let msg = data?.message || ''
|
||
if (data?.error && typeof data.error === 'string') {
|
||
// Go validator errors format: "Key: 'Field' Error:Field validation ..."
|
||
const matches = data.error.match(/Error: (Field validation.*?)(\n|$)/gi)
|
||
if (matches) {
|
||
const fields = matches.map(m => {
|
||
const fieldMatch = m.match(/Key: '([^']+)'/)
|
||
return fieldMatch ? fieldMatch[1] : m
|
||
})
|
||
const labels = { Handler: '经手人', Supplier: '供应商', Receiver: '接收方' }
|
||
msg = fields.map(f => labels[f] || f).join('、') + '不能为空'
|
||
}
|
||
}
|
||
if (!silent) {
|
||
ElMessage.error(hasChinese(msg) ? msg : '请检查必填字段')
|
||
}
|
||
} else {
|
||
if (!silent) {
|
||
ElMessage.error(responseErrorText(error))
|
||
}
|
||
}
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
export default request
|