|
|
@@ -0,0 +1,88 @@
|
|
|
+import { getToken } from '@/store/auth'
|
|
|
+
|
|
|
+const DEFAULT_BASE = ''
|
|
|
+const DEFAULT_TIMEOUT = 15000
|
|
|
+
|
|
|
+export class HttpError extends Error {
|
|
|
+ constructor({ code = 'INTERNAL_ERROR', message = '请求失败', status = 0, payload = null }) {
|
|
|
+ super(message)
|
|
|
+ this.name = 'HttpError'
|
|
|
+ this.code = code
|
|
|
+ this.status = status
|
|
|
+ this.payload = payload
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function resolveBaseURL() {
|
|
|
+ const envBase = import.meta.env.VITE_API_BASE
|
|
|
+ return (envBase ?? DEFAULT_BASE).replace(/\/+$/, '')
|
|
|
+}
|
|
|
+
|
|
|
+function buildURL(path, query) {
|
|
|
+ const base = resolveBaseURL()
|
|
|
+ const cleanPath = path.startsWith('/') ? path : `/${path}`
|
|
|
+ if (!query) return `${base}${cleanPath}`
|
|
|
+ const params = new URLSearchParams()
|
|
|
+ Object.entries(query).forEach(([k, v]) => {
|
|
|
+ if (v === undefined || v === null) return
|
|
|
+ params.append(k, String(v))
|
|
|
+ })
|
|
|
+ const qs = params.toString()
|
|
|
+ return qs ? `${base}${cleanPath}?${qs}` : `${base}${cleanPath}`
|
|
|
+}
|
|
|
+
|
|
|
+async function parseBody(response) {
|
|
|
+ const contentType = response.headers.get('content-type') || ''
|
|
|
+ if (contentType.includes('application/json')) {
|
|
|
+ try { return await response.json() } catch { return null }
|
|
|
+ }
|
|
|
+ try { return await response.text() } catch { return null }
|
|
|
+}
|
|
|
+
|
|
|
+export async function request(path, { method = 'GET', body, query, headers, timeout = DEFAULT_TIMEOUT, auth = true } = {}) {
|
|
|
+ const controller = new AbortController()
|
|
|
+ const timer = setTimeout(() => controller.abort(), timeout)
|
|
|
+
|
|
|
+ const finalHeaders = {
|
|
|
+ Accept: 'application/json',
|
|
|
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
|
+ ...headers,
|
|
|
+ }
|
|
|
+ if (auth) {
|
|
|
+ const token = getToken()
|
|
|
+ if (token) finalHeaders.Authorization = `Bearer ${token}`
|
|
|
+ }
|
|
|
+
|
|
|
+ let response
|
|
|
+ try {
|
|
|
+ response = await fetch(buildURL(path, query), {
|
|
|
+ method,
|
|
|
+ headers: finalHeaders,
|
|
|
+ body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
|
+ signal: controller.signal,
|
|
|
+ })
|
|
|
+ } catch (err) {
|
|
|
+ clearTimeout(timer)
|
|
|
+ if (err?.name === 'AbortError') {
|
|
|
+ throw new HttpError({ code: 'TIMEOUT', message: '请求超时,请稍后重试', status: 0 })
|
|
|
+ }
|
|
|
+ throw new HttpError({ code: 'NETWORK_ERROR', message: '网络异常,请检查连接', status: 0 })
|
|
|
+ }
|
|
|
+ clearTimeout(timer)
|
|
|
+
|
|
|
+ const data = await parseBody(response)
|
|
|
+ if (response.ok) {
|
|
|
+ return data
|
|
|
+ }
|
|
|
+
|
|
|
+ const errCode = (data && typeof data === 'object' && data.code) || `HTTP_${response.status}`
|
|
|
+ const errMsg = (data && typeof data === 'object' && data.message) || response.statusText || '请求失败'
|
|
|
+ throw new HttpError({ code: errCode, message: errMsg, status: response.status, payload: data })
|
|
|
+}
|
|
|
+
|
|
|
+export const http = {
|
|
|
+ get: (path, opts = {}) => request(path, { ...opts, method: 'GET' }),
|
|
|
+ post: (path, body, opts = {}) => request(path, { ...opts, method: 'POST', body }),
|
|
|
+ put: (path, body, opts = {}) => request(path, { ...opts, method: 'PUT', body }),
|
|
|
+ del: (path, opts = {}) => request(path, { ...opts, method: 'DELETE' }),
|
|
|
+}
|