ForcePilot/web/src/apis/base.js
Kris e6e24a91de feat(channels): 多渠道网关模块批量功能与样式优化
本次提交完成多渠道网关模块多项核心优化:
1. 新增公共样式库与二维码生成工具,统一列表页交互风格
2. 重构分页、选择、错误处理等通用composable,收敛重复逻辑
3. 调整白名单列表默认分页大小为20,优化数据展示密度
4. 新增目录搜索建议、批量选择与导出能力
5. 优化发件箱与会话统计接口适配,补充趋势数据获取
6. 重构消息列表模块,支持URL状态同步与多维度筛选
7. 修复API错误处理逻辑,补充traceId日志便于排查问题
8. 调整侧边栏导航结构与折叠状态管理
2026-07-07 16:24:35 +08:00

353 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useUserStore, checkAdminPermission, checkSuperAdminPermission } from '@/stores/user'
import { message } from 'ant-design-vue'
/**
* 基础API请求封装
* 提供统一的请求方法,自动处理认证头和错误
*/
// 默认请求超时时间(毫秒)
const DEFAULT_TIMEOUT = 30000
/**
* 提取并拼接 422 验证错误详情为用户可读的中文提示
* 后端返回格式可能为 { detail: [{ msg, loc }] } 或 { detail: { field: [errors] } } 或字符串
* @param {*} detail - 后端返回的验证错误详情
* @returns {string} 拼接后的错误信息
*/
function formatValidationError(detail) {
if (!detail) return '请求参数验证失败'
if (typeof detail === 'string') return detail
if (Array.isArray(detail)) {
return detail
.map((err) => {
if (typeof err === 'string') return err
const field = err.loc?.slice(-1)?.[0] ?? err.field ?? ''
const msg = err.msg ?? err.message ?? ''
return field ? `${field}: ${msg}` : msg
})
.filter(Boolean)
.join('; ')
}
if (typeof detail === 'object') {
return Object.entries(detail)
.map(([field, errors]) => {
const msg = Array.isArray(errors) ? errors.join(', ') : String(errors)
return `${field}: ${msg}`
})
.join('; ')
}
return '请求参数验证失败'
}
/**
* 发送API请求的基础函数
* @param {string} url - API端点
* @param {Object} options - 请求选项
* @param {boolean} requiresAuth - 是否需要认证头
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
* @returns {Promise} - 请求结果
*/
export async function apiRequest(url, options = {}, requiresAuth = true, responseType = 'json') {
// 超时控制:默认 30s可通过 options.timeout 自定义options.signal 支持外部主动取消
const { timeout = DEFAULT_TIMEOUT, signal: userSignal, ...restOptions } = options
const controller = new AbortController()
let isTimeout = false
const timeoutId = setTimeout(() => {
isTimeout = true
controller.abort()
}, timeout)
// 联动用户传入的 signal支持外部主动取消
if (userSignal) {
if (userSignal.aborted) controller.abort()
else {
const onAbort = () => controller.abort()
userSignal.addEventListener('abort', onAbort, { once: true })
}
}
try {
const isFormData = options?.body instanceof FormData
// 默认请求配置
const requestOptions = {
...restOptions,
signal: controller.signal,
headers: {
...(!isFormData ? { 'Content-Type': 'application/json' } : {}),
...options.headers
}
}
// 如果需要认证,添加认证头
if (requiresAuth) {
const userStore = useUserStore()
if (!userStore.isLoggedIn) {
throw new Error('用户未登录')
}
Object.assign(requestOptions.headers, userStore.getAuthHeaders())
}
// 发送请求
let response
try {
response = await fetch(url, requestOptions)
} catch (fetchError) {
// 超时AbortError 且 isTimeout 标志为真
if (fetchError.name === 'AbortError') {
if (isTimeout) {
message.error('请求超时,请稍后重试')
throw new Error('请求超时,请稍后重试', { cause: fetchError })
}
// 用户主动取消,静默抛出
throw fetchError
}
// TypeError 通常是网络中断 / DNS 失败 / CORS
if (fetchError instanceof TypeError) {
message.error('网络连接失败,请检查网络')
throw new Error('网络连接失败,请检查网络', { cause: fetchError })
}
throw fetchError
}
// 处理API返回的错误
if (!response.ok) {
// 尝试解析错误信息
let errorMessage = `请求失败: ${response.status}, ${response.statusText}`
let errorData = null
try {
errorData = await response.json()
errorMessage = errorData.detail || errorData.message || errorMessage
// 422 验证错误:提取字段错误并提示用户
if (response.status === 422) {
console.error('422验证错误:', {
url,
status: response.status,
responseData: errorData
})
message.error(formatValidationError(errorData.detail))
}
} catch {
// 如果无法解析JSON使用默认错误信息
}
// 特殊处理401和403错误
const error = new Error(errorMessage)
error.response = {
status: response.status,
statusText: response.statusText,
data: errorData
}
if (response.status === 401) {
// 如果是认证失败,可能需要重新登录
const userStore = useUserStore()
// 检查是否是token过期detail 可能为字符串或对象)
const detailStr = typeof errorData?.detail === 'string' ? errorData.detail : ''
const isTokenExpired =
detailStr.includes('令牌已过期') ||
detailStr.includes('token expired') ||
errorMessage?.includes('令牌已过期') ||
errorMessage?.includes('token expired')
message.error(isTokenExpired ? '登录已过期,请重新登录' : '认证失败,请重新登录')
// 如果用户当前认为自己已登录,则登出
if (userStore.isLoggedIn) {
userStore.logout()
}
// 使用setTimeout确保消息显示后再跳转
setTimeout(() => {
window.location.href = '/login'
}, 1500)
throw error
} else if (response.status === 403) {
error.message = '没有权限执行此操作'
throw error
} else if (response.status === 500) {
// 提取后端统一错误响应中的 message 和 trace_id格式{success, error: {code, message, trace_id, details}}
const serverError = errorData?.error
const traceId = serverError?.trace_id
error.message = serverError?.message || '服务器内部错误,请稍后重试'
if (traceId) {
error.traceId = traceId
// 控制台记录 trace_id 便于开发者排查,不暴露给用户
console.error(`[500] trace_id=${traceId}`, { url, status: response.status })
}
throw error
}
throw error
}
// 根据responseType处理响应
if (responseType === 'blob') {
return response
} else if (responseType === 'json') {
// 检查Content-Type以确定如何处理响应
const contentType = response.headers.get('Content-Type')
if (contentType && contentType.includes('application/json')) {
return await response.json()
}
return await response.text()
} else if (responseType === 'text') {
return await response.text()
} else {
return response
}
} catch (error) {
if (error.name !== 'AbortError') {
console.error('API请求错误:', error)
}
throw error
} finally {
clearTimeout(timeoutId)
}
}
/**
* 发送GET请求
* @param {string} url - API端点
* @param {Object} options - 请求选项
* @param {boolean} requiresAuth - 是否需要认证
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
* @returns {Promise} - 请求结果
*/
export function apiGet(url, options = {}, requiresAuth = true, responseType = 'json') {
return apiRequest(url, { method: 'GET', ...options }, requiresAuth, responseType)
}
export function apiAdminGet(url, options = {}, responseType = 'json') {
checkAdminPermission()
return apiGet(url, options, true, responseType)
}
export function apiSuperAdminGet(url, options = {}, responseType = 'json') {
checkSuperAdminPermission()
return apiGet(url, options, true, responseType)
}
/**
* 发送POST请求
* @param {string} url - API端点
* @param {Object} data - 请求体数据
* @param {Object} options - 其他请求选项
* @param {boolean} requiresAuth - 是否需要认证
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
* @returns {Promise} - 请求结果
*/
export function apiPost(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') {
return apiRequest(
url,
{
method: 'POST',
body: data instanceof FormData ? data : JSON.stringify(data),
...options
},
requiresAuth,
responseType
)
}
export function apiAdminPost(url, data = {}, options = {}, responseType = 'json') {
checkAdminPermission()
return apiPost(url, data, options, true, responseType)
}
export function apiSuperAdminPost(url, data = {}, options = {}, responseType = 'json') {
checkSuperAdminPermission()
return apiPost(url, data, options, true, responseType)
}
/**
* 发送PUT请求
* @param {string} url - API端点
* @param {Object} data - 请求体数据
* @param {Object} options - 其他请求选项
* @param {boolean} requiresAuth - 是否需要认证
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
* @returns {Promise} - 请求结果
*/
export function apiPut(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') {
return apiRequest(
url,
{
method: 'PUT',
body: data instanceof FormData ? data : JSON.stringify(data),
...options
},
requiresAuth,
responseType
)
}
export function apiAdminPut(url, data = {}, options = {}, responseType = 'json') {
checkAdminPermission()
return apiPut(url, data, options, true, responseType)
}
export function apiSuperAdminPut(url, data = {}, options = {}, responseType = 'json') {
checkSuperAdminPermission()
return apiPut(url, data, options, true, responseType)
}
/**
* 发送PATCH请求
* @param {string} url - API端点
* @param {Object} data - 请求体数据
* @param {Object} options - 其他请求选项
* @param {boolean} requiresAuth - 是否需要认证
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
* @returns {Promise} - 请求结果
*/
export function apiPatch(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') {
return apiRequest(
url,
{
method: 'PATCH',
body: data instanceof FormData ? data : JSON.stringify(data),
...options
},
requiresAuth,
responseType
)
}
export function apiAdminPatch(url, data = {}, options = {}, responseType = 'json') {
checkAdminPermission()
return apiPatch(url, data, options, true, responseType)
}
export function apiSuperAdminPatch(url, data = {}, options = {}, responseType = 'json') {
checkSuperAdminPermission()
return apiPatch(url, data, options, true, responseType)
}
/**
* 发送DELETE请求
* @param {string} url - API端点
* @param {Object} options - 请求选项
* @param {boolean} requiresAuth - 是否需要认证
* @param {string} responseType - 响应类型: 'json' | 'text' | 'blob'
* @returns {Promise} - 请求结果
*/
export function apiDelete(url, options = {}, requiresAuth = true, responseType = 'json') {
return apiRequest(url, { method: 'DELETE', ...options }, requiresAuth, responseType)
}
export function apiAdminDelete(url, options = {}) {
checkAdminPermission()
return apiDelete(url, options, true)
}
export function apiSuperAdminDelete(url, options = {}) {
checkSuperAdminPermission()
return apiDelete(url, options, true)
}