104 lines
4.3 KiB
JavaScript
104 lines
4.3 KiB
JavaScript
import { ElMessage } from 'element-plus'
|
|
import router from '@/app/router'
|
|
import { useAuthUserStore } from '@/features/auth/store/authUserStore'
|
|
|
|
// 辅助函数:从 document.cookie 中安全地读取指定的 cookie 值
|
|
export function getCookie(name) {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
|
}
|
|
|
|
let isNavigatingToLogin = false; // 增加防重入锁,避免并发 401 触发无限弹窗和跳转
|
|
|
|
export async function request(url, options = {}) {
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...options.headers,
|
|
}
|
|
|
|
const csrfToken = getCookie('csrf_token');
|
|
if (csrfToken) {
|
|
headers['X-CSRF-Token'] = csrfToken;
|
|
}
|
|
|
|
// --- 离线拦截逻辑 ---
|
|
// 拦截破坏性/写操作,在无网络时提供温和提示并短路执行
|
|
const method = (options.method || 'GET').toUpperCase();
|
|
if (!navigator.onLine && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
|
|
if (!options.silent) {
|
|
ElMessage.warning('当前处于离线模式,无法进行此操作');
|
|
}
|
|
throw new Error('Offline: Cannot perform mutative action');
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, { ...options, headers, credentials: 'include' })
|
|
|
|
// 针对 204 No Content,没有 body,直接返回成功
|
|
if (response.status === 204) {
|
|
return { success: true };
|
|
}
|
|
|
|
const data = await response.json()
|
|
|
|
// 处理 401 Unauthorized (后端统一抛出的 AppError 会被 index.ts 捕获并返回 { code: 401, success: false, message: ...})
|
|
if (response.status === 401 || data.code === 401) {
|
|
if (options.silent) {
|
|
throw new Error(data.message || data.error || 'Unauthorized/Forbidden')
|
|
}
|
|
|
|
if (!isNavigatingToLogin) {
|
|
isNavigatingToLogin = true;
|
|
ElMessage.error(data.message || data.error || '会话已过期,请重新登录')
|
|
|
|
// 调用 Pinia Action 清空用户信息、缓存、以及重置内存状态,防止路由守卫死循环打回
|
|
try {
|
|
const authUserStore = useAuthUserStore()
|
|
authUserStore.clearUserInfo()
|
|
} catch (e) {
|
|
// Fallback to manual localstorage clear if store is unavailable
|
|
localStorage.removeItem('userInfo')
|
|
localStorage.removeItem('secure_vault')
|
|
localStorage.removeItem('backup_providers_cache')
|
|
sessionStorage.removeItem('vault_session_key')
|
|
}
|
|
|
|
if (router && router.currentRoute.value.path !== '/login') {
|
|
router.replace('/login').then(() => {
|
|
// 给个延迟再释放锁,防止快速回溯
|
|
setTimeout(() => { isNavigatingToLogin = false }, 1000)
|
|
}).catch(() => {
|
|
setTimeout(() => { isNavigatingToLogin = false }, 1000)
|
|
})
|
|
} else {
|
|
setTimeout(() => { isNavigatingToLogin = false }, 1000)
|
|
}
|
|
}
|
|
throw new Error(data.message || data.error || 'Unauthorized/Forbidden')
|
|
}
|
|
|
|
// 处理其他业务报错或 HTTP 错误
|
|
// 注意:兼容旧的 data.error 和新的 data.message
|
|
if (!response.ok || data.success === false) {
|
|
if (!options.silent) {
|
|
ElMessage.error(data.message || data.error || '请求失败')
|
|
}
|
|
throw new Error(data.message || data.error || '请求失败')
|
|
}
|
|
|
|
// 解构新的标准响应格式。如果后端返回 { success: true, data: [...] },这里直接将 data 铺平返回,保持对原有旧代码的兼容性。
|
|
// 或如果未被包装,直接 fallback 回 data 本身
|
|
if (data.data !== undefined && Object.keys(data).includes('code')) {
|
|
return { success: true, ...data.data }
|
|
}
|
|
|
|
return data
|
|
} catch (error) {
|
|
// 屏蔽被 silent 处理过的 Auth Error
|
|
if (error.message !== 'Unauthorized/Forbidden' && !options.silent) {
|
|
console.error('API Request Error:', error)
|
|
}
|
|
throw error
|
|
}
|
|
} |