member-login-management-system/src/api/request.js

215 lines
5.4 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 { 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