import { message } from 'antd' /** * 统一请求封装 * 基于原生 fetch,支持: * - baseURL(通过 vite 环境变量 VITE_API_BASE_URL 配置) * - 自动注入 token * - 统一响应解析(约定 { code, data, message }) * - 统一错误提示 * - 超时控制 */ const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api' const DEFAULT_TIMEOUT = 15000 const TOKEN_KEY = 'token' export function getToken() { return localStorage.getItem(TOKEN_KEY) } export function setToken(token) { localStorage.setItem(TOKEN_KEY, token) } export function removeToken() { localStorage.removeItem(TOKEN_KEY) } /** * 拼接 query 参数 */ function buildQuery(params) { if (!params || typeof params !== 'object') return '' const usp = new URLSearchParams() Object.entries(params).forEach(([key, value]) => { if (value === undefined || value === null || value === '') return if (Array.isArray(value)) { value.forEach((v) => usp.append(key, v)) } else { usp.append(key, value) } }) const str = usp.toString() return str ? `?${str}` : '' } /** * fetch 超时封装 */ function fetchWithTimeout(url, options, timeout) { const controller = new AbortController() const timer = setTimeout(() => controller.abort(), timeout) return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timer) ) } /** * 统一错误处理 */ function handleError(error) { if (error?.name === 'AbortError') { message.error('请求超时,请稍后重试') } else if (error?.silent) { // 静默错误,不弹提示 } else { message.error(error?.message || '网络异常') } return Promise.reject(error) } /** * 处理未授权 */ function handleUnauthorized() { removeToken() message.error('登录已失效,请重新登录') // 避免循环跳转 if (window.location.pathname !== '/login') { window.location.href = '/login' } } /** * 核心请求方法 * @param {string} url - 接口路径 * @param {object} options - 配置项 * @param {'GET'|'POST'|'PUT'|'DELETE'|'PATCH'} options.method * @param {object} options.params - URL 参数 * @param {object|FormData} options.data - 请求体 * @param {object} options.headers - 自定义 headers * @param {number} options.timeout - 超时时间(ms) * @param {boolean} options.raw - 为 true 时返回完整响应不做业务解析 * @param {boolean} options.silent - 为 true 时静默错误(不弹提示) */ export async function request(url, options = {}) { const { method = 'GET', params, data, headers = {}, timeout = DEFAULT_TIMEOUT, raw = false, silent = false, } = options const token = getToken() const finalHeaders = { Accept: 'application/json', ...headers, } if (token) { finalHeaders.Authorization = `Bearer ${token}` } let body if (data !== undefined && data !== null) { if (data instanceof FormData) { body = data // FormData 由浏览器自动设置 Content-Type } else { finalHeaders['Content-Type'] = finalHeaders['Content-Type'] || 'application/json;charset=UTF-8' body = JSON.stringify(data) } } const fullUrl = `${BASE_URL}${url}${buildQuery(params)}` try { const res = await fetchWithTimeout( fullUrl, { method, headers: finalHeaders, body, credentials: 'include', }, timeout ) // 401 未授权 if (res.status === 401) { handleUnauthorized() return Promise.reject({ message: '未授权', silent: true }) } if (raw) { return res } const contentType = res.headers.get('content-type') || '' let payload if (contentType.includes('application/json')) { payload = await res.json() } else { payload = await res.text() } if (!res.ok) { const err = { message: payload?.message || `请求失败 (${res.status})`, status: res.status, data: payload, silent, } return handleError(err) } // 业务约定:{ success, code, data, message } 或 { code, data, message } // - success === true 表示成功 // - 或 code === 0 / 200 表示成功 if (payload && typeof payload === 'object' && ('success' in payload || 'code' in payload)) { const isSuccess = payload.success === true || payload.code === 0 || payload.code === 200 || payload.code === '0' || payload.code === '200' if (isSuccess) { return payload.data } return handleError({ message: payload.message || payload.msg || '操作失败', code: payload.code, data: payload.data, silent, }) } // 非约定结构直接返回 return payload } catch (error) { if (error && error.silent === undefined) { error.silent = silent } return handleError(error) } } /** * 快捷方法 */ export const http = { get: (url, params, options = {}) => request(url, { ...options, method: 'GET', params }), post: (url, data, options = {}) => request(url, { ...options, method: 'POST', data }), put: (url, data, options = {}) => request(url, { ...options, method: 'PUT', data }), patch: (url, data, options = {}) => request(url, { ...options, method: 'PATCH', data }), delete: (url, params, options = {}) => request(url, { ...options, method: 'DELETE', params }), } export default http