203 lines
5.8 KiB
JavaScript
203 lines
5.8 KiB
JavaScript
import axios from 'axios'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import router from '@/router'
|
|
import NProgress from 'nprogress'
|
|
|
|
// 創建 axios 實例
|
|
const service = axios.create({
|
|
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:12010/api/v1',
|
|
timeout: 30000, // 30秒超時
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
|
|
// 請求攔截器
|
|
service.interceptors.request.use(
|
|
config => {
|
|
NProgress.start()
|
|
|
|
|
|
// JWT 認證:添加 Authorization header
|
|
const authStore = useAuthStore()
|
|
if (authStore.token) {
|
|
config.headers.Authorization = `Bearer ${authStore.token}`
|
|
}
|
|
|
|
return config
|
|
},
|
|
error => {
|
|
NProgress.done()
|
|
console.error('❌ [Request Error]:', error)
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
// 用於防止重複處理 401 錯誤
|
|
let isHandling401 = false
|
|
|
|
// 回應攔截器
|
|
service.interceptors.response.use(
|
|
response => {
|
|
NProgress.done()
|
|
|
|
console.log('✅ [API Response]', {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
url: response.config.url,
|
|
method: response.config.method.toUpperCase(),
|
|
data: response.data,
|
|
headers: response.headers,
|
|
timestamp: new Date().toISOString()
|
|
})
|
|
|
|
const { data } = response
|
|
|
|
// 後端統一回應格式處理
|
|
if (data && typeof data === 'object') {
|
|
if (data.success === false) {
|
|
// 業務錯誤處理
|
|
const message = data.message || '操作失敗'
|
|
console.warn('⚠️ [Business Error]:', message)
|
|
ElMessage.error(message)
|
|
return Promise.reject(new Error(message))
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
return response
|
|
},
|
|
error => {
|
|
NProgress.done()
|
|
|
|
const { response } = error
|
|
const authStore = useAuthStore()
|
|
|
|
if (response) {
|
|
const { status, data } = response
|
|
|
|
switch (status) {
|
|
case 401:
|
|
// 避免在登入頁面或登入過程中觸發自動登出
|
|
const requestUrl = error.config?.url || ''
|
|
const currentPath = router.currentRoute.value.path
|
|
|
|
console.error('🔐 [401 Unauthorized]', {
|
|
requestUrl,
|
|
currentPath,
|
|
isLoginPage: currentPath === '/login',
|
|
isLoginRequest: requestUrl.includes('/auth/login'),
|
|
isHandling401,
|
|
willTriggerLogout: currentPath !== '/login' && !requestUrl.includes('/auth/login') && !isHandling401,
|
|
timestamp: new Date().toISOString()
|
|
})
|
|
|
|
// 防止重複處理
|
|
if (!isHandling401 && currentPath !== '/login' && !requestUrl.includes('/auth/login')) {
|
|
isHandling401 = true
|
|
console.error('🚪 [Auto Logout] 認證失效,觸發自動登出')
|
|
|
|
// 只顯示一次訊息
|
|
ElMessage.error('認證已過期,請重新登入')
|
|
|
|
// 使用自動登出模式,不顯示額外訊息
|
|
authStore.logout(false, true).finally(() => {
|
|
router.push('/login').then(() => {
|
|
// 導航完成後重置標記
|
|
setTimeout(() => {
|
|
isHandling401 = false
|
|
}, 1000)
|
|
})
|
|
})
|
|
} else if (isHandling401) {
|
|
console.log('🔐 [401 Skipped] 已在處理其他 401 錯誤')
|
|
} else {
|
|
console.log('🔐 [401 Ignored] 在登入頁面或登入請求')
|
|
}
|
|
break
|
|
|
|
case 403:
|
|
ElMessage.error('無權限存取此資源')
|
|
break
|
|
|
|
case 404:
|
|
ElMessage.error('請求的資源不存在')
|
|
break
|
|
|
|
case 422:
|
|
// 表單驗證錯誤
|
|
const message = data.message || '輸入資料格式錯誤'
|
|
ElMessage.error(message)
|
|
break
|
|
|
|
case 429:
|
|
ElMessage.error('請求過於頻繁,請稍後再試')
|
|
break
|
|
|
|
case 500:
|
|
ElMessage.error('伺服器內部錯誤')
|
|
break
|
|
|
|
case 502:
|
|
case 503:
|
|
case 504:
|
|
ElMessage.error('伺服器暫時無法存取,請稍後再試')
|
|
break
|
|
|
|
default:
|
|
const errorMessage = data?.message || error.message || '網路錯誤'
|
|
ElMessage.error(errorMessage)
|
|
}
|
|
} else if (error.code === 'ECONNABORTED') {
|
|
ElMessage.error('請求超時,請檢查網路連線')
|
|
} else {
|
|
ElMessage.error('網路連線失敗,請檢查網路設定')
|
|
}
|
|
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
// 檔案上傳專用請求實例
|
|
export const uploadRequest = axios.create({
|
|
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:12010/api/v1',
|
|
timeout: 120000, // 2分鐘超時
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data'
|
|
}
|
|
})
|
|
|
|
// 為上傳請求添加攔截器
|
|
uploadRequest.interceptors.request.use(
|
|
config => {
|
|
// JWT 認證:添加 Authorization header
|
|
const authStore = useAuthStore()
|
|
if (authStore.token) {
|
|
config.headers.Authorization = `Bearer ${authStore.token}`
|
|
}
|
|
return config
|
|
},
|
|
error => Promise.reject(error)
|
|
)
|
|
|
|
uploadRequest.interceptors.response.use(
|
|
response => response.data,
|
|
error => {
|
|
const message = error.response?.data?.message || '檔案上傳失敗'
|
|
ElMessage.error(message)
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
// 常用請求方法封裝
|
|
export const request = {
|
|
get: (url, config = {}) => service.get(url, config),
|
|
post: (url, data = {}, config = {}) => service.post(url, data, config),
|
|
put: (url, data = {}, config = {}) => service.put(url, data, config),
|
|
delete: (url, config = {}) => service.delete(url, config),
|
|
patch: (url, data = {}, config = {}) => service.patch(url, data, config)
|
|
}
|
|
|
|
export default service |